抓取生产工单,赚取发料异常
This commit is contained in:
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
9
.idea/Datie.iml
generated
Normal file
9
.idea/Datie.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/misc.xml
generated
Normal file
6
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_24" default="true" project-jdk-name="24" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/Datie.iml" filepath="$PROJECT_DIR$/.idea/Datie.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
7
.idea/vcs.xml
generated
Normal file
7
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
42
browser_login/analyze_duplicates.py
Normal file
42
browser_login/analyze_duplicates.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import json
|
||||||
|
from collections import defaultdict
|
||||||
|
from config import OUTPUT_DIR
|
||||||
|
|
||||||
|
filepath = OUTPUT_DIR / "issue_receipt_details_full.json"
|
||||||
|
|
||||||
|
with open(filepath, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# 用来记录每个组合出现的次数和对应的列表索引
|
||||||
|
seen = defaultdict(list)
|
||||||
|
null_keys = 0
|
||||||
|
|
||||||
|
for idx, item in enumerate(data):
|
||||||
|
wo = item.get("发料单号")
|
||||||
|
line = item.get("行号")
|
||||||
|
mat = item.get("物料代码")
|
||||||
|
|
||||||
|
if not wo or not line or not mat:
|
||||||
|
null_keys += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = f"{wo}_{line}_{mat}"
|
||||||
|
seen[key].append(idx)
|
||||||
|
|
||||||
|
duplicates = {k: v for k, v in seen.items() if len(v) > 1}
|
||||||
|
|
||||||
|
print(f"总数据条数: {len(data)}")
|
||||||
|
print(f"缺失关键字段的数据条数: {null_keys}")
|
||||||
|
print(f"发现重复的组合数: {len(duplicates)}")
|
||||||
|
redundant_count = sum(len(v)-1 for v in duplicates.values())
|
||||||
|
print(f"因重复而多出的冗余条数: {redundant_count}")
|
||||||
|
|
||||||
|
# 打印前 5 个重复的例子
|
||||||
|
count = 0
|
||||||
|
for k, indices in duplicates.items():
|
||||||
|
if count >= 5:
|
||||||
|
break
|
||||||
|
print(f"\n重复键 (发料单号_行号_物料代码): {k}")
|
||||||
|
print(f" 第一次出现在第 {indices[0] + 1} 条,最新状态: {data[indices[0]].get('状态')}")
|
||||||
|
print(f" 第二次出现在第 {indices[1] + 1} 条,最新状态: {data[indices[1]].get('状态')}")
|
||||||
|
count += 1
|
||||||
249
browser_login/auto_fetch_abnormal_report.py
Normal file
249
browser_login/auto_fetch_abnormal_report.py
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
import datetime
|
||||||
|
import calendar
|
||||||
|
import json
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
from config import OUTPUT_DIR
|
||||||
|
|
||||||
|
def navigate_to_report(page):
|
||||||
|
print("正在打开主页...")
|
||||||
|
page.get("https://yunmes.tftykj.cn/")
|
||||||
|
page.wait.load_start()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
print("正在打开 生产工单发料异常检查报表...")
|
||||||
|
try:
|
||||||
|
m1 = page.ele('text=自定义报表管理')
|
||||||
|
if m1:
|
||||||
|
print("点击第一级...")
|
||||||
|
m1.click()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 找到展开后的第二级
|
||||||
|
for m in page.eles('text:自定义报表管理'):
|
||||||
|
try:
|
||||||
|
m.click()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
for m in page.eles('text:自定义报表'):
|
||||||
|
if m.text == '自定义报表':
|
||||||
|
try:
|
||||||
|
m.click()
|
||||||
|
print("点击第三级...")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
ele = page.ele('text:生产工单发料异常检查报表', timeout=5)
|
||||||
|
if ele:
|
||||||
|
print("找到报表行,选中...")
|
||||||
|
ele.parent('tag:tr').click()
|
||||||
|
time.sleep(0.5)
|
||||||
|
btn = page.ele('text=进入自定义报表')
|
||||||
|
if btn:
|
||||||
|
print("点击进入自定义报表...")
|
||||||
|
btn.click()
|
||||||
|
time.sleep(3)
|
||||||
|
print("成功进入报表!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("未找到进入按钮。")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print("未能找到 '生产工单发料异常检查报表'")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"执行导航过程中发生异常: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def fetch_report_data(page):
|
||||||
|
# Wait for the new tab to be ready
|
||||||
|
time.sleep(3)
|
||||||
|
target_tab = page.get_tab(page.latest_tab)
|
||||||
|
|
||||||
|
# Wait for the label to appear
|
||||||
|
target_tab.ele('text:下单日期(开始)', timeout=10)
|
||||||
|
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
first_day = datetime.date(now.year, now.month, 1).strftime('%Y-%m-%d')
|
||||||
|
last_day = datetime.date(now.year, now.month, calendar.monthrange(now.year, now.month)[1]).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
print(f"设置下单日期为当月: {first_day} 至 {last_day},并清理发料情况过滤条件...")
|
||||||
|
|
||||||
|
# 使用注入到全部 iframe 的 JS 强制执行 EasyUI 方法
|
||||||
|
target_tab.run_js(f"""
|
||||||
|
var iframes = document.querySelectorAll('iframe');
|
||||||
|
for(var j=0; j<iframes.length; j++) {{
|
||||||
|
try {{
|
||||||
|
var doc = iframes[j].contentDocument || iframes[j].contentWindow.document;
|
||||||
|
var win = iframes[j].contentWindow;
|
||||||
|
|
||||||
|
// 1. 设置开始日期
|
||||||
|
var startInputs = doc.querySelectorAll('.input_StartValue.datebox-f');
|
||||||
|
if (startInputs.length > 0) {{
|
||||||
|
win.$(startInputs[0]).datebox('setValue', '{first_day}');
|
||||||
|
}}
|
||||||
|
|
||||||
|
// 2. 设置结束日期
|
||||||
|
var endInputs = doc.querySelectorAll('.input_EndValue.datebox-f');
|
||||||
|
if (endInputs.length > 0) {{
|
||||||
|
win.$(endInputs[0]).datebox('setValue', '{last_day}');
|
||||||
|
}}
|
||||||
|
|
||||||
|
// 3. 清理所有下拉框(包括发料情况)
|
||||||
|
var combos = doc.querySelectorAll('.combobox-f, .textbox-f');
|
||||||
|
for(var i=0; i<combos.length; i++) {{
|
||||||
|
try {{ win.$(combos[i]).combobox('clear'); }} catch(e) {{}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// 4. [提速黑科技]:强行把每页请求的数量从 50 条改为 500 条
|
||||||
|
// 找到底部的分页组件并修改它的 pageSize,这样点击查询时就会一次请求 500 条
|
||||||
|
var paginations = doc.querySelectorAll('.pagination');
|
||||||
|
for(var i=0; i<paginations.length; i++) {{
|
||||||
|
try {{ win.$(paginations[i]).pagination({{pageSize: 500}}); }} catch(e) {{}}
|
||||||
|
}}
|
||||||
|
}} catch(e) {{}}
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
|
||||||
|
print("日期和条件设置完成,准备查询...")
|
||||||
|
|
||||||
|
print("正在查找并点击查询按钮...")
|
||||||
|
|
||||||
|
# 因为查询按钮在 iframe 里,我们不能直接用 target_tab 找,必须用 JS 去触发点击
|
||||||
|
target_tab.listen.start()
|
||||||
|
|
||||||
|
target_tab.run_js("""
|
||||||
|
var iframes = document.querySelectorAll('iframe');
|
||||||
|
var clicked = false;
|
||||||
|
for(var j=0; j<iframes.length; j++) {
|
||||||
|
try {
|
||||||
|
var doc = iframes[j].contentDocument || iframes[j].contentWindow.document;
|
||||||
|
var btn = doc.querySelector('#onSearch');
|
||||||
|
if(!btn) {
|
||||||
|
var spans = doc.querySelectorAll('.l-btn-text');
|
||||||
|
for(var i=0; i<spans.length; i++) {
|
||||||
|
if(spans[i].innerText === '查询') {
|
||||||
|
btn = spans[i].parentNode.parentNode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(btn) {
|
||||||
|
btn.click();
|
||||||
|
console.log('Clicked search button inside iframe');
|
||||||
|
clicked = true;
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
return clicked;
|
||||||
|
""")
|
||||||
|
|
||||||
|
print("点击指令已发送,等待报表数据加载 (3秒)...")
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
current_page = 1
|
||||||
|
total_inserted = 0
|
||||||
|
|
||||||
|
print("开始监听网络请求,寻找 API 数据包...")
|
||||||
|
while True:
|
||||||
|
packets = target_tab.listen.steps()
|
||||||
|
print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 收集到 {len(packets)} 个网络数据包,正在解析...")
|
||||||
|
|
||||||
|
found_data = False
|
||||||
|
total_pages = 1
|
||||||
|
|
||||||
|
for p in packets:
|
||||||
|
if 'SearchCustomReportBySQL_Proxy' in p.url or 'CustomTableViewData' in p.url or 'SeachList' in p.url:
|
||||||
|
print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 命中目标 URL: {p.url[:100]}...")
|
||||||
|
if p.method == 'POST' and p.response and p.response.body:
|
||||||
|
print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 这是一个 POST 请求,且包含 response body")
|
||||||
|
try:
|
||||||
|
body = p.response.body
|
||||||
|
data = body if isinstance(body, (dict, list)) else json.loads(body)
|
||||||
|
|
||||||
|
if isinstance(data, dict) and 'result' in data and isinstance(data['result'], dict) and 'items' in data['result']:
|
||||||
|
res = data['result']
|
||||||
|
total_count = res.get('totalCount', 0)
|
||||||
|
items = res.get('items', [])
|
||||||
|
|
||||||
|
print("===================================")
|
||||||
|
print(f"✅ 成功拦截到报表数据API (第 {current_page} 页)")
|
||||||
|
print(f"✅ 数据总条数: {total_count}, 当前页条数: {len(items)}")
|
||||||
|
print("===================================")
|
||||||
|
|
||||||
|
total_pages = (total_count + 499) // 500 if total_count > 0 else 1
|
||||||
|
|
||||||
|
# Import and save to database
|
||||||
|
try:
|
||||||
|
import import_to_sqlite
|
||||||
|
if items:
|
||||||
|
inserted = import_to_sqlite.import_abnormal_report_data(items)
|
||||||
|
total_inserted += inserted
|
||||||
|
print(f"✅ 成功将本页 {inserted} 条异常报表数据存入数据库")
|
||||||
|
except Exception as db_err:
|
||||||
|
print(f"❌ 保存异常报表数据到数据库失败: {db_err}")
|
||||||
|
|
||||||
|
found_data = True
|
||||||
|
else:
|
||||||
|
print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 数据结构不匹配。")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 解析数据包出错: {e}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not found_data:
|
||||||
|
print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 第 {current_page} 页等待了3秒,没有拦截到匹配的报表数据...")
|
||||||
|
|
||||||
|
# 再给一次机会等3秒
|
||||||
|
print("再等待3秒重试...")
|
||||||
|
time.sleep(3)
|
||||||
|
retry_packets = target_tab.listen.steps()
|
||||||
|
print(f"重试收集到 {len(retry_packets)} 个数据包。")
|
||||||
|
if not retry_packets:
|
||||||
|
print(f"彻底没有数据,停止抓取。")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
packets.extend(retry_packets)
|
||||||
|
# 重新让上面解析
|
||||||
|
continue
|
||||||
|
|
||||||
|
if current_page >= total_pages:
|
||||||
|
print(f"已到达最后一页 (共 {total_pages} 页),抓取完成!")
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f"准备抓取下一页 (第 {current_page + 1} 页)...")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 尝试点击下一页 (同样需要穿透 iframe)
|
||||||
|
target_tab.run_js("""
|
||||||
|
var iframes = document.querySelectorAll('iframe');
|
||||||
|
for(var j=0; j<iframes.length; j++) {
|
||||||
|
try {
|
||||||
|
var doc = iframes[j].contentDocument || iframes[j].contentWindow.document;
|
||||||
|
var nextBtn = doc.querySelector('.pagination-next');
|
||||||
|
if(nextBtn && nextBtn.tagName === 'SPAN') {
|
||||||
|
nextBtn = nextBtn.parentNode;
|
||||||
|
}
|
||||||
|
if(nextBtn) {
|
||||||
|
nextBtn.click();
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 等待新的网络请求
|
||||||
|
time.sleep(2)
|
||||||
|
current_page += 1
|
||||||
|
|
||||||
|
print(f"🎉 异常报表全量抓取大功告成!总计入库: {total_inserted} 条。")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
page = get_page(port=9222)
|
||||||
|
success = navigate_to_report(page)
|
||||||
|
if success:
|
||||||
|
fetch_report_data(page)
|
||||||
56
browser_login/auto_navigate_report.py
Normal file
56
browser_login/auto_navigate_report.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
|
||||||
|
print("正在打开主页...")
|
||||||
|
page.get("https://yunmes.tftykj.cn/")
|
||||||
|
page.wait.load_start()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
print("正在打开 生产工单发料异常检查报表...")
|
||||||
|
try:
|
||||||
|
m1 = page.ele('text=自定义报表管理')
|
||||||
|
if m1:
|
||||||
|
print("点击第一级...")
|
||||||
|
m1.click()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 找到展开后的第二级
|
||||||
|
for m in page.eles('text:自定义报表管理'):
|
||||||
|
try:
|
||||||
|
m.click()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
for m in page.eles('text:自定义报表'):
|
||||||
|
if m.text == '自定义报表':
|
||||||
|
try:
|
||||||
|
m.click()
|
||||||
|
print("点击第三级...")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
ele = page.ele('text:生产工单发料异常检查报表', timeout=5)
|
||||||
|
if ele:
|
||||||
|
print("找到报表行,选中...")
|
||||||
|
ele.parent('tag:tr').click()
|
||||||
|
time.sleep(0.5)
|
||||||
|
btn = page.ele('text=进入自定义报表')
|
||||||
|
if btn:
|
||||||
|
print("点击进入自定义报表...")
|
||||||
|
btn.click()
|
||||||
|
time.sleep(2)
|
||||||
|
print("成功进入报表!")
|
||||||
|
else:
|
||||||
|
print("未找到进入按钮。")
|
||||||
|
else:
|
||||||
|
print("未能找到 '生产工单发料异常检查报表'")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"执行过程中发生异常: {e}")
|
||||||
|
|
||||||
18
browser_login/check_page.py
Normal file
18
browser_login/check_page.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
import time
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
print("当前 URL:", page.url)
|
||||||
|
print("当前 标题:", page.title)
|
||||||
|
|
||||||
|
# 如果已经进入了自定义报表,搜索“生产工单发料异常检查报表”
|
||||||
|
search_box = page.ele('xpath://input[@placeholder="请输入查询内容"]') # 假设有这个
|
||||||
|
if search_box:
|
||||||
|
print("找到搜索框")
|
||||||
|
else:
|
||||||
|
inputs = page.eles('tag:input')
|
||||||
|
print(f"页面上有 {len(inputs)} 个输入框")
|
||||||
|
|
||||||
9
browser_login/check_tab.py
Normal file
9
browser_login/check_tab.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
tabs = page.get_tabs()
|
||||||
|
for t in tabs:
|
||||||
|
print("Tab URL:", t.url)
|
||||||
26
browser_login/click_report.py
Normal file
26
browser_login/click_report.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
|
||||||
|
# Click the first one
|
||||||
|
m1 = page.ele('text=自定义报表管理')
|
||||||
|
print("Clicking:", m1.html)
|
||||||
|
m1.click()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Check what new menus appeared
|
||||||
|
for m in page.eles('text:自定义报表'):
|
||||||
|
print("Found after click:", m.html)
|
||||||
|
if '自定义报表' in m.text:
|
||||||
|
m.click()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
for m in page.eles('text:自定义报表'):
|
||||||
|
print("Found after second click:", m.html)
|
||||||
|
if m.text == '自定义报表':
|
||||||
|
m.click()
|
||||||
|
|
||||||
17
browser_login/click_report_tab.py
Normal file
17
browser_login/click_report_tab.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
|
||||||
|
for m in page.eles('text:自定义报表'):
|
||||||
|
if m.text == '自定义报表':
|
||||||
|
print("Clicking:", m.html)
|
||||||
|
m.click()
|
||||||
|
time.sleep(2)
|
||||||
|
break
|
||||||
|
|
||||||
|
print("当前 URL:", page.url)
|
||||||
|
|
||||||
45
browser_login/do_search.py
Normal file
45
browser_login/do_search.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
from config import OUTPUT_DIR
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
|
||||||
|
# 在当前页面找
|
||||||
|
print("当前报表 URL:", page.url)
|
||||||
|
|
||||||
|
btn = page.ele('#onSearch')
|
||||||
|
if btn:
|
||||||
|
print("点击查询按钮")
|
||||||
|
page.listen.start()
|
||||||
|
btn.click()
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
packets = page.listen.steps()
|
||||||
|
for p in packets:
|
||||||
|
if p.method == 'POST' and ('api' in p.url.lower() or 'Report' in p.url or 'Data' in p.url or 'Search' in p.url):
|
||||||
|
if p.response and p.response.body:
|
||||||
|
try:
|
||||||
|
body = p.response.body
|
||||||
|
data = body if isinstance(body, (dict, list)) else json.loads(body)
|
||||||
|
if isinstance(data, dict) and 'result' in data:
|
||||||
|
res = data['result']
|
||||||
|
print("拦截到数据API:", p.url)
|
||||||
|
print("总条数:", res.get('totalCount'))
|
||||||
|
|
||||||
|
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
path = OUTPUT_DIR / f"report_abnormal_{ts}.json"
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
print("数据已保存至:", path)
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("没有找到匹配的数据")
|
||||||
|
else:
|
||||||
|
print("没有找到 查询 按钮")
|
||||||
52
browser_login/do_search2.py
Normal file
52
browser_login/do_search2.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
from config import OUTPUT_DIR
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
target_tab = page.get_tab(page.latest_tab)
|
||||||
|
|
||||||
|
print("当前报表 URL:", target_tab.url)
|
||||||
|
|
||||||
|
btn = target_tab.ele('#onSearch') or target_tab.ele('text=查询')
|
||||||
|
if btn:
|
||||||
|
print("找到按钮", btn.html)
|
||||||
|
print("点击查询按钮...")
|
||||||
|
target_tab.listen.start()
|
||||||
|
try:
|
||||||
|
btn.click(by_js=True)
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
target_tab.run_js('arguments[0].click()', btn)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
packets = target_tab.listen.steps()
|
||||||
|
for p in packets:
|
||||||
|
if p.method == 'POST' and ('api' in p.url.lower() or 'Report' in p.url or 'Data' in p.url or 'Search' in p.url):
|
||||||
|
if p.response and p.response.body:
|
||||||
|
try:
|
||||||
|
body = p.response.body
|
||||||
|
data = body if isinstance(body, (dict, list)) else json.loads(body)
|
||||||
|
if isinstance(data, dict) and 'result' in data:
|
||||||
|
res = data['result']
|
||||||
|
print("拦截到数据API:", p.url)
|
||||||
|
print("总条数:", res.get('totalCount'))
|
||||||
|
|
||||||
|
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
path = OUTPUT_DIR / f"report_abnormal_{ts}.json"
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
print("数据已保存至:", path)
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("没有找到匹配的数据")
|
||||||
|
else:
|
||||||
|
print("没有找到 查询 按钮")
|
||||||
36
browser_login/enter_report.py
Normal file
36
browser_login/enter_report.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
|
||||||
|
# 找到对应的行
|
||||||
|
ele = page.ele('text:生产工单发料异常检查报表')
|
||||||
|
if ele:
|
||||||
|
print("找到了报表")
|
||||||
|
row = ele.parent('tag:tr')
|
||||||
|
print("行 HTML:", row.html)
|
||||||
|
|
||||||
|
# 尝试双击该行,或者找按钮
|
||||||
|
btns = row.eles('tag:button')
|
||||||
|
if btns:
|
||||||
|
for b in btns:
|
||||||
|
print("按钮:", b.text, b.html)
|
||||||
|
if '进入' in b.text or '查看' in b.text or '打开' in b.text:
|
||||||
|
b.click()
|
||||||
|
print("点击了按钮")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("没找到按钮,尝试点击该行然后找进入按钮")
|
||||||
|
row.click() # 先选中
|
||||||
|
time.sleep(0.5)
|
||||||
|
btn_enter = page.ele('text=进入报表') or page.ele('text=进入') or page.ele('text=查看')
|
||||||
|
if btn_enter:
|
||||||
|
print("找到顶部的按钮:", btn_enter.html)
|
||||||
|
btn_enter.click()
|
||||||
|
else:
|
||||||
|
print("也没找到顶部的进入按钮,尝试双击行")
|
||||||
|
row.click()
|
||||||
|
row.click()
|
||||||
25
browser_login/enter_report2.py
Normal file
25
browser_login/enter_report2.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
|
||||||
|
# 1. 选中该行
|
||||||
|
ele = page.ele('text:生产工单发料异常检查报表')
|
||||||
|
if ele:
|
||||||
|
print("找到了报表,正在选中该行...")
|
||||||
|
ele.parent('tag:tr').click()
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# 2. 点击进入
|
||||||
|
btn = page.ele('text=进入自定义报表')
|
||||||
|
if btn:
|
||||||
|
print("点击进入自定义报表...")
|
||||||
|
btn.click()
|
||||||
|
time.sleep(2)
|
||||||
|
print("进入成功!")
|
||||||
|
else:
|
||||||
|
print("没找到 进入自定义报表 按钮")
|
||||||
|
|
||||||
81
browser_login/fetch_abnormal_report.py
Normal file
81
browser_login/fetch_abnormal_report.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
import datetime
|
||||||
|
import calendar
|
||||||
|
import json
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
from config import OUTPUT_DIR
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
target_tab = page.get_tab(page.latest_tab)
|
||||||
|
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
first_day = datetime.date(now.year, now.month, 1).strftime('%Y-%m-%d')
|
||||||
|
last_day = datetime.date(now.year, now.month, calendar.monthrange(now.year, now.month)[1]).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
print(f"Setting dates: {first_day} to {last_day}")
|
||||||
|
|
||||||
|
start_label = target_tab.ele('text:下单日期(开始)')
|
||||||
|
if start_label:
|
||||||
|
input_ele = start_label.next().ele('tag:input')
|
||||||
|
if input_ele:
|
||||||
|
input_ele.run_js(f"this.value = '{first_day}';")
|
||||||
|
hidden_input = start_label.next().ele('css:input[type="hidden"]')
|
||||||
|
if hidden_input:
|
||||||
|
hidden_input.run_js(f"this.value = '{first_day}';")
|
||||||
|
visible_input = start_label.next().ele('css:input.textbox-text')
|
||||||
|
if visible_input:
|
||||||
|
visible_input.run_js(f"this.value = '{first_day}';")
|
||||||
|
|
||||||
|
end_label = target_tab.ele('text:下单日期(结束)')
|
||||||
|
if end_label:
|
||||||
|
input_ele = end_label.next().ele('tag:input')
|
||||||
|
if input_ele:
|
||||||
|
input_ele.run_js(f"this.value = '{last_day}';")
|
||||||
|
hidden_input = end_label.next().ele('css:input[type="hidden"]')
|
||||||
|
if hidden_input:
|
||||||
|
hidden_input.run_js(f"this.value = '{last_day}';")
|
||||||
|
visible_input = end_label.next().ele('css:input.textbox-text')
|
||||||
|
if visible_input:
|
||||||
|
visible_input.run_js(f"this.value = '{last_day}';")
|
||||||
|
|
||||||
|
print("Dates set.")
|
||||||
|
|
||||||
|
btn = target_tab.ele('#onSearch') or target_tab.ele('text=查询')
|
||||||
|
if btn:
|
||||||
|
target_tab.listen.start()
|
||||||
|
try:
|
||||||
|
btn.click(by_js=True)
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
btn.run_js('this.click()')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
packets = target_tab.listen.steps()
|
||||||
|
for p in packets:
|
||||||
|
if p.method == 'POST' and ('api' in p.url.lower() or 'Report' in p.url or 'Data' in p.url or 'Search' in p.url):
|
||||||
|
if p.response and p.response.body:
|
||||||
|
try:
|
||||||
|
body = p.response.body
|
||||||
|
data = body if isinstance(body, (dict, list)) else json.loads(body)
|
||||||
|
if isinstance(data, dict) and 'result' in data:
|
||||||
|
res = data['result']
|
||||||
|
print("拦截到数据API:", p.url)
|
||||||
|
print("总条数:", res.get('totalCount'))
|
||||||
|
|
||||||
|
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
path = OUTPUT_DIR / f"report_abnormal_{ts}.json"
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
print("数据已保存至:", path)
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("没有找到匹配的数据")
|
||||||
|
else:
|
||||||
|
print("没有找到查询按钮")
|
||||||
147
browser_login/fetch_basis_quality_incremental.py
Normal file
147
browser_login/fetch_basis_quality_incremental.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
"""
|
||||||
|
质量报表 (Basis Quality Report) - 时间窗口滑动增量抓取
|
||||||
|
目标: 采用底层请求拦截与篡改技术,强行指定“下单日期(开始)”为特定的时间窗口,抓取数据。
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import urllib.parse
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page, log
|
||||||
|
from config import OUTPUT_DIR
|
||||||
|
|
||||||
|
HOME_URL = "https://yunmes.tftykj.cn/"
|
||||||
|
API_TARGET = "SearchCustomReportBySQL_Proxy"
|
||||||
|
SAVE_PATH = OUTPUT_DIR / "basis_quality_incremental.json"
|
||||||
|
|
||||||
|
def fetch_basis_quality_incremental():
|
||||||
|
# 动态计算时间窗口(使用内置的 timedelta 计算过去 90 天,避免依赖外部库)
|
||||||
|
end_date = datetime.now()
|
||||||
|
start_date = end_date - timedelta(days=90)
|
||||||
|
start_date_str = start_date.strftime("%Y-%m-%d 00:00:00")
|
||||||
|
end_date_str = end_date.strftime("%Y-%m-%d 23:59:59")
|
||||||
|
|
||||||
|
# URL 编码
|
||||||
|
encoded_start = urllib.parse.quote(start_date_str)
|
||||||
|
encoded_end = urllib.parse.quote(end_date_str)
|
||||||
|
|
||||||
|
log("INFO", f"=== 🚀 启动质量报表 - 时间滑动增量抓取 ===")
|
||||||
|
log("INFO", f"📅 设定的滑动窗口: {start_date_str} -> {end_date_str}")
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
all_clean_items = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
log("INFO", f"正在回到主页起点: {HOME_URL}")
|
||||||
|
page.get(HOME_URL)
|
||||||
|
page.wait.load_start()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
menus = [
|
||||||
|
("进入质量报表", 'xpath://*[@id="el-collapse-content-21"]/div/div/div/div[1]/div/div/div[6]/div')
|
||||||
|
]
|
||||||
|
|
||||||
|
log("INFO", "开始模拟人工点击左侧导航菜单...")
|
||||||
|
for name, xpath in menus:
|
||||||
|
ele = page.ele(xpath, timeout=5)
|
||||||
|
if ele:
|
||||||
|
try: ele.click()
|
||||||
|
except: page.run_js("arguments[0].click();", ele)
|
||||||
|
else:
|
||||||
|
log("ERR", f"找不到菜单元素: {name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
log("OK", "✅ 成功点开质量报表界面!")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# 开启普通的数据监听
|
||||||
|
log("INFO", f"开启底层拦截网: {API_TARGET}")
|
||||||
|
page.listen.start(API_TARGET)
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# 循环翻页抓取逻辑 (测试模式:仅抓取前 3 页)
|
||||||
|
# =========================================================
|
||||||
|
current_page = 1
|
||||||
|
query_btn_xpath = 'xpath://*[@id="customTable-search-area"]/div[1]/div/div[1]/a[2]/span/span'
|
||||||
|
|
||||||
|
while current_page <= 3: # 限制只抓取前 3 页用于测试
|
||||||
|
# 1. 因为我们无法用 DrissionPage 的 listen 修改发送出去的 POST Data
|
||||||
|
# 我们直接在 Python 层发送一个 JS Fetch 请求,完全模拟原有的请求,但带上我们自己构造的 Payload!
|
||||||
|
log("INFO", f"正在通过底层 JS Fetch 强行注入带时间窗口的请求... (页码: {current_page})")
|
||||||
|
|
||||||
|
# 注意:这里的 new_payload 必须转义所有的单双引号以适配 JS 字符串拼接
|
||||||
|
base_payload = f"page={current_page}&rows=50&id=80&sqlFilter%5BfieldList%5D%5B0%5D%5Bid%5D=17647&sqlFilter%5BfieldList%5D%5B0%5D%5Bfield%5D=%E4%B8%8B%E5%8D%95%E6%97%A5%E6%9C%9F(%E7%BB%93%E6%9D%9F)&sqlFilter%5BfieldList%5D%5B0%5D%5BfieldTranslate%5D=%5B%E4%B8%8B%E5%8D%95%E6%97%A5%E6%9C%9F(%E7%BB%93%E6%9D%9F)%5D&sqlFilter%5BfieldList%5D%5B0%5D%5BstartValue%5D={encoded_end}&sqlFilter%5BfieldList%5D%5B0%5D%5BendValue%5D=&sqlFilter%5BfieldList%5D%5B0%5D%5BcompareEnum%5D=0&sqlFilter%5BfieldList%5D%5B0%5D%5BfieldDataType%5D=2&sqlFilter%5BfieldList%5D%5B0%5D%5BorderNumber%5D=&sqlFilter%5BfieldList%5D%5B0%5D%5BorderType%5D=0&sqlFilter%5BfieldList%5D%5B0%5D%5BisTimeLimit%5D=false&sqlFilter%5BfieldList%5D%5B0%5D%5BlimitLength%5D=0&sqlFilter%5BfieldList%5D%5B0%5D%5BdateType%5D=1&sqlFilter%5BfieldList%5D%5B0%5D%5BdateDefaultType%5D=0&sqlFilter%5BfieldList%5D%5B0%5D%5BisSqlField%5D=false&sqlFilter%5BfieldList%5D%5B0%5D%5Bcondition%5D=0&sqlFilter%5BfieldList%5D%5B0%5D%5BgetValue%5D=&sqlFilter%5BfieldList%5D%5B0%5D%5BbackgroundColor%5D=&sqlFilter%5BfieldList%5D%5B0%5D%5BfontColor%5D=&sqlFilter%5BfieldList%5D%5B0%5D%5BisSeachParam%5D=true&sqlFilter%5BfieldList%5D%5B0%5D%5BdefaultValue%5D=&sqlFilter%5BfieldList%5D%5B0%5D%5Bwidth%5D=&sqlFilter%5BfieldList%5D%5B0%5D%5BdefaultTime%5D=&sqlFilter%5BfieldList%5D%5B0%5D%5BsearchParamEnableVal%5D=0&sqlFilter%5BfieldList%5D%5B0%5D%5BoptionMode%5D=0&sqlFilter%5BfieldList%5D%5B1%5D%5Bid%5D=17646&sqlFilter%5BfieldList%5D%5B1%5D%5Bfield%5D=%E4%B8%8B%E5%8D%95%E6%97%A5%E6%9C%9F(%E5%BC%80%E5%A7%8B)&sqlFilter%5BfieldList%5D%5B1%5D%5BfieldTranslate%5D=%5B%E4%B8%8B%E5%8D%95%E6%97%A5%E6%9C%9F(%E5%BC%80%E5%A7%8B)%5D&sqlFilter%5BfieldList%5D%5B1%5D%5BstartValue%5D={encoded_start}&sqlFilter%5BfieldList%5D%5B1%5D%5BendValue%5D=&sqlFilter%5BfieldList%5D%5B1%5D%5BcompareEnum%5D=0&sqlFilter%5BfieldList%5D%5B1%5D%5BfieldDataType%5D=2&sqlFilter%5BfieldList%5D%5B1%5D%5BorderNumber%5D=&sqlFilter%5BfieldList%5D%5B1%5D%5BorderType%5D=0&sqlFilter%5BfieldList%5D%5B1%5D%5BisTimeLimit%5D=false&sqlFilter%5BfieldList%5D%5B1%5D%5BlimitLength%5D=0&sqlFilter%5BfieldList%5D%5B1%5D%5BdateType%5D=1&sqlFilter%5BfieldList%5D%5B1%5D%5BdateDefaultType%5D=0&sqlFilter%5BfieldList%5D%5B1%5D%5BisSqlField%5D=false&sqlFilter%5BfieldList%5D%5B1%5D%5Bcondition%5D=0&sqlFilter%5BfieldList%5D%5B1%5D%5BgetValue%5D=&sqlFilter%5BfieldList%5D%5B1%5D%5BbackgroundColor%5D=&sqlFilter%5BfieldList%5D%5B1%5D%5BfontColor%5D=&sqlFilter%5BfieldList%5D%5B1%5D%5BisSeachParam%5D=true&sqlFilter%5BfieldList%5D%5B1%5D%5BdefaultValue%5D=&sqlFilter%5BfieldList%5D%5B1%5D%5Bwidth%5D=&sqlFilter%5BfieldList%5D%5B1%5D%5BdefaultTime%5D=&sqlFilter%5BfieldList%5D%5B1%5D%5BsearchParamEnableVal%5D=1&sqlFilter%5BfieldList%5D%5B1%5D%5BoptionMode%5D=0&isAll=false"
|
||||||
|
|
||||||
|
# 强行在页面中注入一个 Fetch 请求。由于在页面上下文中运行,它会自动带上所有的 Cookies 和 Auth Token!
|
||||||
|
fetch_js = f"""
|
||||||
|
fetch('/api/services/TfTechApi/SQLSolution/SearchCustomReportBySQL_Proxy', {{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {{
|
||||||
|
'accept': 'application/json, text/javascript, */*; q=0.01',
|
||||||
|
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||||
|
'x-requested-with': 'XMLHttpRequest'
|
||||||
|
}},
|
||||||
|
body: '{base_payload}'
|
||||||
|
}});
|
||||||
|
"""
|
||||||
|
page.run_js(fetch_js)
|
||||||
|
|
||||||
|
# 2. 等待我们注入的请求响应
|
||||||
|
packet = page.listen.wait(timeout=15)
|
||||||
|
if not packet:
|
||||||
|
log("ERR", f"第 {current_page} 页注入请求超时或未触发,中止抓取。")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 3. 解析数据
|
||||||
|
body = packet.response.body
|
||||||
|
data = body if isinstance(body, (dict, list)) else json.loads(body)
|
||||||
|
|
||||||
|
if isinstance(data, dict) and "result" in data:
|
||||||
|
# 检查 result 是否是字典,如果直接是列表则直接取用
|
||||||
|
if isinstance(data["result"], dict):
|
||||||
|
items = data["result"].get("items", [])
|
||||||
|
elif isinstance(data["result"], list):
|
||||||
|
items = data["result"]
|
||||||
|
else:
|
||||||
|
items = []
|
||||||
|
|
||||||
|
if not items:
|
||||||
|
log("WARN", f"第 {current_page} 页返回了空列表,可能该时间段内无数据。")
|
||||||
|
break
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
all_clean_items.append(item)
|
||||||
|
|
||||||
|
log("OK", f"第 {current_page} 页清洗完成,累计提取 {len(all_clean_items)} 条数据。")
|
||||||
|
|
||||||
|
if current_page % 10 == 0:
|
||||||
|
with open(SAVE_PATH, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(all_clean_items, f, ensure_ascii=False, indent=2)
|
||||||
|
else:
|
||||||
|
log("ERR", f"第 {current_page} 页数据结构异常,中止。")
|
||||||
|
break
|
||||||
|
|
||||||
|
current_page += 1
|
||||||
|
|
||||||
|
# 最终保存
|
||||||
|
if all_clean_items:
|
||||||
|
with open(SAVE_PATH, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(all_clean_items, f, ensure_ascii=False, indent=2)
|
||||||
|
log("OK", f"🎉 抓取完成!总计成功提取 {len(all_clean_items)} 条数据。")
|
||||||
|
log("OK", f"数据已保存至: {SAVE_PATH}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log("ERR", f"发生全局异常: {e}")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
page.listen.stop()
|
||||||
|
log("INFO", "🛑 已释放浏览器监听资源。")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fetch_basis_quality_incremental()
|
||||||
109
browser_login/fetch_basis_quality_sample.py
Normal file
109
browser_login/fetch_basis_quality_sample.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
"""
|
||||||
|
质量报表 (Basis Quality Report) - 样本抓取脚本
|
||||||
|
目标: 模拟点击菜单进入页面,拦截 BasisQualityReport_GetValueFieldListNew_Proxy 接口,提取前 5 条数据进行结构分析。
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page, log
|
||||||
|
from config import OUTPUT_DIR
|
||||||
|
|
||||||
|
HOME_URL = "https://yunmes.tftykj.cn/"
|
||||||
|
API_TARGET = "SearchCustomReportBySQL_Proxy"
|
||||||
|
SAVE_PATH = OUTPUT_DIR / "basis_quality_sample.json"
|
||||||
|
|
||||||
|
def fetch_basis_quality_sample():
|
||||||
|
log("INFO", "=== 🧪 启动质量报表样本抓取 (前5条) ===")
|
||||||
|
page = get_page(port=9222)
|
||||||
|
|
||||||
|
try:
|
||||||
|
log("INFO", f"正在回到主页起点: {HOME_URL}")
|
||||||
|
page.get(HOME_URL)
|
||||||
|
page.wait.load_start()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
menus = [
|
||||||
|
("进入质量报表", 'xpath://*[@id="el-collapse-content-21"]/div/div/div/div[1]/div/div/div[6]/div')
|
||||||
|
]
|
||||||
|
|
||||||
|
# 核心修改:因为数据是一进页面就加载,所以必须在点击菜单【之前】就开始监听!
|
||||||
|
log("INFO", f"开启底层数据拦截网: {API_TARGET} (提前开启,以防错过初始加载)")
|
||||||
|
page.listen.start(API_TARGET)
|
||||||
|
|
||||||
|
log("INFO", "开始模拟人工点击左侧导航菜单...")
|
||||||
|
for name, xpath in menus:
|
||||||
|
ele = page.ele(xpath, timeout=5)
|
||||||
|
if ele:
|
||||||
|
try: ele.click()
|
||||||
|
except: page.run_js("arguments[0].click();", ele)
|
||||||
|
else:
|
||||||
|
log("ERR", f"找不到菜单元素: {name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
log("OK", "✅ 成功点开质量报表界面!")
|
||||||
|
|
||||||
|
# 尝试点击空白处隐藏可能遮挡的菜单 (根据实际情况可能需要调整或注释掉)
|
||||||
|
blank_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[1]/div[2]/div[2]/div/div[1]/div'
|
||||||
|
blank_ele = page.ele(blank_xpath, timeout=3)
|
||||||
|
if blank_ele:
|
||||||
|
try: blank_ele.click()
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
log("INFO", "等待拦截初始加载的数据包...")
|
||||||
|
packet = page.listen.wait(timeout=15)
|
||||||
|
|
||||||
|
if not packet:
|
||||||
|
log("ERR", "未能拦截到数据请求,可能网络超时。")
|
||||||
|
page.listen.stop()
|
||||||
|
return
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# 数据处理
|
||||||
|
# =========================================================
|
||||||
|
log("OK", f"🎉 成功拦截到数据!HTTP 状态码: {packet.response.status}")
|
||||||
|
body = packet.response.body
|
||||||
|
data = body if isinstance(body, (dict, list)) else json.loads(body)
|
||||||
|
|
||||||
|
sample_items = []
|
||||||
|
|
||||||
|
if isinstance(data, dict) and "result" in data:
|
||||||
|
# 根据提供的 curl 参数 {"wageCalculationPlanId":80,"basisQualityReportId":23}
|
||||||
|
# 这个接口的返回结构可能与之前的 SearchList 不同,这里做个宽泛的判断
|
||||||
|
items = data["result"]
|
||||||
|
|
||||||
|
# 如果 result 直接是个列表,或者里面包着 items 列表
|
||||||
|
if isinstance(items, dict) and "items" in items:
|
||||||
|
items = items["items"]
|
||||||
|
elif not isinstance(items, list):
|
||||||
|
# 如果既不是列表也不是包含 items 的字典,就把整个 result 放进去看看
|
||||||
|
items = [items]
|
||||||
|
|
||||||
|
log("INFO", f"本页包含 {len(items)} 条数据,准备提取前 5 条。")
|
||||||
|
|
||||||
|
for item in items[:5]:
|
||||||
|
sample_items.append(item)
|
||||||
|
|
||||||
|
with open(SAVE_PATH, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(sample_items, f, ensure_ascii=False, indent=2)
|
||||||
|
log("OK", f"💾 样本提取完成!已保存 {len(sample_items)} 条记录至: {SAVE_PATH}")
|
||||||
|
else:
|
||||||
|
log("ERR", "返回的数据结构中找不到 'result' 节点。")
|
||||||
|
# 把原始结构存下来方便分析
|
||||||
|
with open(SAVE_PATH, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
log("INFO", f"已将原始返回数据保存至: {SAVE_PATH} 以供分析。")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log("ERR", f"发生全局异常: {e}")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
page.listen.stop()
|
||||||
|
log("INFO", "🛑 已释放浏览器监听资源。")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fetch_basis_quality_sample()
|
||||||
297
browser_login/fetch_issue_receipt_details.py
Normal file
297
browser_login/fetch_issue_receipt_details.py
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
"""
|
||||||
|
发料单报表 - 导航测试脚本
|
||||||
|
目标: 模拟点击菜单,进入“发料单报表”页面。
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page, log
|
||||||
|
from config import OUTPUT_DIR
|
||||||
|
|
||||||
|
HOME_URL = "https://yunmes.tftykj.cn/"
|
||||||
|
API_TARGET = "WorkOrdersDetailed_SearchListAll_Proxy"
|
||||||
|
SAVE_PATH = OUTPUT_DIR / "issue_receipt_details_full.json"
|
||||||
|
|
||||||
|
def fetch_issue_receipt_details():
|
||||||
|
log("INFO", "=== 🚀 启动发料单报表全量数据抓取 ===")
|
||||||
|
# 强制复用 9222 端口,不关闭浏览器
|
||||||
|
page = get_page(port=9222)
|
||||||
|
|
||||||
|
all_clean_items = []
|
||||||
|
if SAVE_PATH.exists():
|
||||||
|
try:
|
||||||
|
with open(SAVE_PATH, "r", encoding="utf-8") as f:
|
||||||
|
all_clean_items = json.load(f)
|
||||||
|
log("INFO", f"📦 已加载本地历史存档,包含 {len(all_clean_items)} 条数据。")
|
||||||
|
except Exception as e:
|
||||||
|
log("WARN", f"加载本地存档失败: {e},将从空列表开始。")
|
||||||
|
all_clean_items = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
log("INFO", f"正在回到主页起点: {HOME_URL}")
|
||||||
|
page.get(HOME_URL)
|
||||||
|
page.wait.load_start()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
menus = [
|
||||||
|
("第一层: 业务统计报表", 'xpath://*[@id="app"]/div/div[1]/div[1]/div[2]/div/div[1]/div/div[10]/div/p'),
|
||||||
|
("第二层: 生产业务报表(推测)", 'xpath:/html/body/div[7]/div/div[1]/div/div[9]/div/p'),
|
||||||
|
("第三层: 发料单报表", 'xpath:/html/body/div[8]/div/div[1]/div/div[6]/div/p')
|
||||||
|
]
|
||||||
|
|
||||||
|
log("INFO", "开始模拟人工点击左侧导航菜单...")
|
||||||
|
for name, xpath in menus:
|
||||||
|
ele = page.ele(xpath, timeout=5)
|
||||||
|
if ele:
|
||||||
|
try:
|
||||||
|
ele.click()
|
||||||
|
except:
|
||||||
|
page.run_js("arguments[0].click();", ele)
|
||||||
|
time.sleep(1.5)
|
||||||
|
else:
|
||||||
|
log("ERR", f"找不到菜单元素: {name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
log("OK", "✅ 成功点开发料单报表界面!")
|
||||||
|
|
||||||
|
# 点击空白处隐藏菜单
|
||||||
|
blank_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[1]/div[2]/div[2]/div/div[1]/div'
|
||||||
|
blank_ele = page.ele(blank_xpath, timeout=3)
|
||||||
|
if blank_ele:
|
||||||
|
try:
|
||||||
|
blank_ele.click()
|
||||||
|
except:
|
||||||
|
page.run_js("arguments[0].click();", blank_ele)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
log("INFO", f"开启底层数据拦截网: {API_TARGET}")
|
||||||
|
page.listen.start(API_TARGET)
|
||||||
|
|
||||||
|
# 等待页面自动发起的请求
|
||||||
|
packet = page.listen.wait(timeout=10)
|
||||||
|
|
||||||
|
if not packet:
|
||||||
|
log("INFO", "尝试寻找并点击页面上的【查询】按钮...")
|
||||||
|
query_btn_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[2]/div[1]/div[1]/div/button[1]/span'
|
||||||
|
query_btn = page.ele(query_btn_xpath, timeout=3)
|
||||||
|
|
||||||
|
if query_btn:
|
||||||
|
try: query_btn.click()
|
||||||
|
except: page.run_js("arguments[0].click();", query_btn)
|
||||||
|
packet = page.listen.wait(timeout=15)
|
||||||
|
|
||||||
|
if not packet:
|
||||||
|
log("ERR", "未能拦截到数据请求,可能网络超时或查询未触发。")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 设定开始抓取的页码,如果因为中断需要断点续传,请修改此变量
|
||||||
|
# 刚才抓到了 95 页,我们需要从 96 页开始继续
|
||||||
|
target_resume_page = 1
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# 第一页数据处理
|
||||||
|
# =========================================================
|
||||||
|
log("OK", f"🎉 成功拦截到第一页数据!HTTP 状态码: {packet.response.status}")
|
||||||
|
body = packet.response.body
|
||||||
|
data = body if isinstance(body, (dict, list)) else json.loads(body)
|
||||||
|
|
||||||
|
total_count = 0
|
||||||
|
if isinstance(data, dict) and "result" in data:
|
||||||
|
total_count = data["result"].get("totalCount", 0)
|
||||||
|
items = data["result"].get("items", [])
|
||||||
|
log("OK", f"后端报告总条数: {total_count}")
|
||||||
|
|
||||||
|
# 只有当不是断点续传(即从第1页开始)时,才把第一页的数据加入列表
|
||||||
|
if target_resume_page <= 1:
|
||||||
|
# 由于可能触发断点,如果是重新抓取,这里直接覆盖
|
||||||
|
if not all_clean_items:
|
||||||
|
for item in items:
|
||||||
|
all_clean_items.append(_extract_fields(item))
|
||||||
|
log("OK", f"第一页清洗完成,提取了 {len(items)} 条数据。")
|
||||||
|
else:
|
||||||
|
log("INFO", f"本地已有数据,跳过第一页保存,走翻页逻辑(注意:发料单可能需要您清空旧存档才能从头抓,这里先保留累加)")
|
||||||
|
else:
|
||||||
|
log("INFO", f"触发断点续传,跳过第一页的数据保存。后端报告总条数: {total_count}")
|
||||||
|
else:
|
||||||
|
log("ERR", "第一页返回的数据结构异常。")
|
||||||
|
return
|
||||||
|
|
||||||
|
page_num = 1
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# 断点续传逻辑跳转
|
||||||
|
# =========================================================
|
||||||
|
if target_resume_page > 1:
|
||||||
|
log("INFO", f"🚀 触发断点续传机制!准备直接跳转到第 {target_resume_page} 页...")
|
||||||
|
# 尝试找页码输入框
|
||||||
|
jumper_input_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[2]/div[1]/div[2]/div/div[2]/div[1]/span[3]/div/div//input'
|
||||||
|
input_ele = page.ele(jumper_input_xpath, timeout=5)
|
||||||
|
|
||||||
|
if not input_ele:
|
||||||
|
jumper_input_xpath = 'xpath://input[@type="number" and @aria-label="页"]'
|
||||||
|
input_ele = page.ele(jumper_input_xpath, timeout=5)
|
||||||
|
|
||||||
|
if input_ele:
|
||||||
|
input_ele.clear()
|
||||||
|
input_ele.input(str(target_resume_page))
|
||||||
|
time.sleep(0.5)
|
||||||
|
input_ele.input('\n')
|
||||||
|
|
||||||
|
packet = page.listen.wait(timeout=15)
|
||||||
|
if not packet:
|
||||||
|
log("ERR", "断点跳转失败,未拦截到目标页的数据请求。")
|
||||||
|
return
|
||||||
|
|
||||||
|
log("OK", f"✅ 成功跳转至第 {target_resume_page} 页并截获数据!")
|
||||||
|
page_num = target_resume_page
|
||||||
|
|
||||||
|
# 读取并解析断点页的数据
|
||||||
|
body = packet.response.body
|
||||||
|
data = body if isinstance(body, (dict, list)) else json.loads(body)
|
||||||
|
if isinstance(data, dict) and "result" in data:
|
||||||
|
items = data["result"].get("items", [])
|
||||||
|
for item in items:
|
||||||
|
all_clean_items.append(_extract_fields(item))
|
||||||
|
log("OK", f"第 {page_num} 页清洗完成,累计提取 {len(all_clean_items)} 条数据。")
|
||||||
|
else:
|
||||||
|
log("ERR", "找不到页码输入框,断点跳转失败,将从第 1 页继续!")
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# 循环翻页抓取
|
||||||
|
# =========================================================
|
||||||
|
while True:
|
||||||
|
# 引入“类人”随机延迟
|
||||||
|
delay = random.uniform(2.5, 5.5)
|
||||||
|
log("INFO", f"⏳ 模拟真人停顿 {delay:.2f} 秒后,准备点击下一页...")
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
if page_num > 1 and page_num % 50 == 0:
|
||||||
|
long_delay = random.uniform(10.0, 20.0)
|
||||||
|
log("INFO", f"☕️ 已经连续高强度翻了 {page_num} 页,触发风控规避机制,假装喝水休息 {long_delay:.2f} 秒...")
|
||||||
|
time.sleep(long_delay)
|
||||||
|
|
||||||
|
# 用户指定的下一页按钮 xpath
|
||||||
|
next_btn_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[2]/div[1]/div[2]/div/div[2]/div[1]/button[2]'
|
||||||
|
next_btn = page.ele(next_btn_xpath, timeout=3)
|
||||||
|
|
||||||
|
if not next_btn:
|
||||||
|
log("ERR", "找不到下一页按钮,尝试强制刷新页面或终止。")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 检查按钮是否被禁用
|
||||||
|
class_str = str(next_btn.attr("class"))
|
||||||
|
aria_disabled = next_btn.attr("aria-disabled")
|
||||||
|
is_disabled_attr = next_btn.attr("disabled") is not None
|
||||||
|
|
||||||
|
if "disabled" in class_str or is_disabled_attr or aria_disabled == "true":
|
||||||
|
log("OK", "🏁 下一页按钮已被禁用,说明已经到达最后一页!")
|
||||||
|
break
|
||||||
|
|
||||||
|
page_num += 1
|
||||||
|
log("INFO", f"正在点击【下一页】抓取第 {page_num} 页...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
next_btn.click()
|
||||||
|
except Exception as e:
|
||||||
|
log("ERR", f"普通点击失败: {e},尝试 JS 点击...")
|
||||||
|
page.run_js("arguments[0].click();", next_btn)
|
||||||
|
|
||||||
|
# 等待新一页的 API 响应
|
||||||
|
packet = page.listen.wait(timeout=15)
|
||||||
|
if not packet:
|
||||||
|
log("ERR", f"第 {page_num} 页请求超时或未触发,中止抓取。")
|
||||||
|
break
|
||||||
|
|
||||||
|
body = packet.response.body
|
||||||
|
data = body if isinstance(body, (dict, list)) else json.loads(body)
|
||||||
|
|
||||||
|
if isinstance(data, dict) and "result" in data:
|
||||||
|
items = data["result"].get("items", [])
|
||||||
|
if not items:
|
||||||
|
log("WARN", f"第 {page_num} 页返回了空列表,可能已无数据。")
|
||||||
|
break
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
all_clean_items.append(_extract_fields(item))
|
||||||
|
log("OK", f"第 {page_num} 页清洗完成,累计提取 {len(all_clean_items)} 条数据。")
|
||||||
|
|
||||||
|
# 每 10 页自动保存一次
|
||||||
|
if page_num % 10 == 0:
|
||||||
|
with open(SAVE_PATH, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(all_clean_items, f, ensure_ascii=False, indent=2)
|
||||||
|
log("INFO", f"💾 自动存档: 已保存 {len(all_clean_items)} 条记录至本地。")
|
||||||
|
else:
|
||||||
|
log("ERR", f"第 {page_num} 页数据结构异常,中止。")
|
||||||
|
break
|
||||||
|
|
||||||
|
page.listen.stop()
|
||||||
|
|
||||||
|
# 最终保存
|
||||||
|
if all_clean_items:
|
||||||
|
with open(SAVE_PATH, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(all_clean_items, f, ensure_ascii=False, indent=2)
|
||||||
|
log("OK", f"🎉 全部抓取完成!总计成功提取 {len(all_clean_items)} 条数据。")
|
||||||
|
log("OK", f"数据已保存至: {SAVE_PATH}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log("ERR", f"发生全局异常: {e}")
|
||||||
|
if all_clean_items:
|
||||||
|
rescue_path = OUTPUT_DIR / "issue_receipt_details_RESCUE.json"
|
||||||
|
with open(rescue_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(all_clean_items, f, ensure_ascii=False, indent=2)
|
||||||
|
log("INFO", f"🆘 触发异常保存,抢救了 {len(all_clean_items)} 条数据。")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
page.listen.stop()
|
||||||
|
log("INFO", "🛑 已释放浏览器监听资源,保持浏览器开启。")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _extract_fields(item):
|
||||||
|
"""提取所需的字段"""
|
||||||
|
return {
|
||||||
|
"生产任务单号": item.get("productionOrderNo"),
|
||||||
|
"生产物料代码": item.get("productMaterialCode"),
|
||||||
|
"生产物料名称": item.get("productMaterialName"),
|
||||||
|
"生产物料规格": item.get("productMaterialSpecification"),
|
||||||
|
"发料单号": item.get("workOrdersNumber"),
|
||||||
|
"状态": item.get("status"),
|
||||||
|
"物料规格": item.get("materialSpecification"),
|
||||||
|
"物料名称": item.get("materialName"),
|
||||||
|
"物料代码": item.get("materialCode"),
|
||||||
|
"发料数量": item.get("issueNumber"),
|
||||||
|
"已发料数量": item.get("hasIssueNumber"),
|
||||||
|
"金额": item.get("amount"),
|
||||||
|
"成本价": item.get("costPrice"),
|
||||||
|
"发料金额": item.get("issueAmount"),
|
||||||
|
"生产订单备注": item.get("productionOrderRemark"),
|
||||||
|
"明细备注": item.get("detailedRemark"),
|
||||||
|
"单位名称": item.get("unitName"),
|
||||||
|
"仓库名称": item.get("warehouseName"),
|
||||||
|
"行号": item.get("lineNumber"),
|
||||||
|
"发料单备注": item.get("workOrdersRemark"),
|
||||||
|
"执行人名称": item.get("executorUserName"),
|
||||||
|
"物料型号": item.get("materialModel"),
|
||||||
|
"执行时间": item.get("executionTime"),
|
||||||
|
"领料人": item.get("materialsUserName"),
|
||||||
|
"生产物料型号": item.get("productMaterialModel"),
|
||||||
|
"自定义字段": item.get("customField"),
|
||||||
|
"部门代码": item.get("departmentInformationCode"),
|
||||||
|
"部门名称": item.get("departmentInformationName"),
|
||||||
|
"图片文件": item.get("imageFile"),
|
||||||
|
"汇总金额": item.get("issueAmountTotal"),
|
||||||
|
"物料组代码": item.get("materialGroupCode"),
|
||||||
|
"物料组名称": item.get("materialGroupName"),
|
||||||
|
"单价小数位数": item.get("numnberOfReservedDigits"),
|
||||||
|
"单价进位策略": item.get("placeMentStrategy"),
|
||||||
|
"单价": item.get("price"),
|
||||||
|
"销售订单号": item.get("salesOrderCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fetch_issue_receipt_details()
|
||||||
297
browser_login/fetch_issue_receipt_incremental.py
Normal file
297
browser_login/fetch_issue_receipt_incremental.py
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
"""
|
||||||
|
发料单报表 - 智能增量同步脚本 (从第一页开始抓,遇到旧数据即停)
|
||||||
|
目标:
|
||||||
|
1. 自动连接本地 SQLite 数据库查询是否存在某条记录。
|
||||||
|
2. 进入 ERP 系统截获发料单数据,由于新数据都在第一页,我们从第 1 页开始抓。
|
||||||
|
3. 逐条对比,如果发现某页的数据在本地已经存在,则认为增量部分已经抓取完毕,提前终止。
|
||||||
|
4. 将新增数据存入 SQLite。
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page, log
|
||||||
|
from config import DB_PATH
|
||||||
|
|
||||||
|
HOME_URL = "https://yunmes.tftykj.cn/"
|
||||||
|
API_TARGET = "WorkOrdersDetailed_SearchListAll_Proxy"
|
||||||
|
|
||||||
|
def get_local_count(conn):
|
||||||
|
"""获取本地数据库已有的总记录数"""
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM issue_receipt_details")
|
||||||
|
return cursor.fetchone()[0]
|
||||||
|
|
||||||
|
def item_exists(cursor, item):
|
||||||
|
"""判断某条发料明细是否已在数据库中存在(基于发料单号+行号+物料代码组合判断)"""
|
||||||
|
wo_number = item.get("workOrdersNumber")
|
||||||
|
line_no = item.get("lineNumber")
|
||||||
|
mat_code = item.get("materialCode")
|
||||||
|
|
||||||
|
# 增加一个容错判断,如果其中有 None 就不当作重复
|
||||||
|
if not wo_number or not line_no or not mat_code:
|
||||||
|
return False
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT 1 FROM issue_receipt_details
|
||||||
|
WHERE work_orders_number = ? AND line_number = ? AND material_code = ?
|
||||||
|
''', (wo_number, line_no, mat_code))
|
||||||
|
return cursor.fetchone() is not None
|
||||||
|
|
||||||
|
def _extract_fields(item):
|
||||||
|
"""提取所需的字段"""
|
||||||
|
return {
|
||||||
|
"生产任务单号": item.get("productionOrderNo"),
|
||||||
|
"生产物料代码": item.get("productMaterialCode"),
|
||||||
|
"生产物料名称": item.get("productMaterialName"),
|
||||||
|
"生产物料规格": item.get("productMaterialSpecification"),
|
||||||
|
"发料单号": item.get("workOrdersNumber"),
|
||||||
|
"状态": item.get("status"),
|
||||||
|
"物料规格": item.get("materialSpecification"),
|
||||||
|
"物料名称": item.get("materialName"),
|
||||||
|
"物料代码": item.get("materialCode"),
|
||||||
|
"发料数量": item.get("issueNumber"),
|
||||||
|
"已发料数量": item.get("hasIssueNumber"),
|
||||||
|
"金额": item.get("amount"),
|
||||||
|
"成本价": item.get("costPrice"),
|
||||||
|
"发料金额": item.get("issueAmount"),
|
||||||
|
"生产订单备注": item.get("productionOrderRemark"),
|
||||||
|
"明细备注": item.get("detailedRemark"),
|
||||||
|
"单位名称": item.get("unitName"),
|
||||||
|
"仓库名称": item.get("warehouseName"),
|
||||||
|
"行号": item.get("lineNumber"),
|
||||||
|
"发料单备注": item.get("workOrdersRemark"),
|
||||||
|
"执行人名称": item.get("executorUserName"),
|
||||||
|
"物料型号": item.get("materialModel"),
|
||||||
|
"执行时间": item.get("executionTime"),
|
||||||
|
"领料人": item.get("materialsUserName"),
|
||||||
|
"生产物料型号": item.get("productMaterialModel"),
|
||||||
|
"自定义字段": item.get("customField"),
|
||||||
|
"部门代码": item.get("departmentInformationCode"),
|
||||||
|
"部门名称": item.get("departmentInformationName"),
|
||||||
|
"图片文件": item.get("imageFile"),
|
||||||
|
"汇总金额": item.get("issueAmountTotal"),
|
||||||
|
"物料组代码": item.get("materialGroupCode"),
|
||||||
|
"物料组名称": item.get("materialGroupName"),
|
||||||
|
"单价小数位数": item.get("numnberOfReservedDigits"),
|
||||||
|
"单价进位策略": item.get("placeMentStrategy"),
|
||||||
|
"单价": item.get("price"),
|
||||||
|
"销售订单号": item.get("salesOrderCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
def fetch_issue_receipt_incremental():
|
||||||
|
log("INFO", "=== 🚀 启动发料单报表 - 智能增量同步 (首屏更新模式) ===")
|
||||||
|
|
||||||
|
if not DB_PATH.exists():
|
||||||
|
log("ERR", f"找不到数据库文件: {DB_PATH},请先执行全量导入!")
|
||||||
|
return
|
||||||
|
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
local_count = get_local_count(conn)
|
||||||
|
log("INFO", f"📦 本地数据库当前总计: {local_count} 条数据")
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
|
||||||
|
try:
|
||||||
|
log("INFO", f"正在回到主页起点: {HOME_URL}")
|
||||||
|
page.get(HOME_URL)
|
||||||
|
page.wait.load_start()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
menus = [
|
||||||
|
("第一层: 业务统计报表", 'xpath://*[@id="app"]/div/div[1]/div[1]/div[2]/div/div[1]/div/div[10]/div/p'),
|
||||||
|
("第二层: 生产业务报表(推测)", 'xpath:/html/body/div[7]/div/div[1]/div/div[9]/div/p'),
|
||||||
|
("第三层: 发料单报表", 'xpath:/html/body/div[8]/div/div[1]/div/div[6]/div/p')
|
||||||
|
]
|
||||||
|
|
||||||
|
log("INFO", "模拟点击左侧导航菜单...")
|
||||||
|
for name, xpath in menus:
|
||||||
|
ele = page.ele(xpath, timeout=5)
|
||||||
|
if ele:
|
||||||
|
try: ele.click()
|
||||||
|
except: page.run_js("arguments[0].click();", ele)
|
||||||
|
time.sleep(1.5)
|
||||||
|
else:
|
||||||
|
log("ERR", f"找不到菜单元素: {name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
log("OK", "✅ 成功点开发料单报表界面!")
|
||||||
|
|
||||||
|
# 隐藏菜单
|
||||||
|
blank_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[1]/div[2]/div[2]/div/div[1]/div'
|
||||||
|
blank_ele = page.ele(blank_xpath, timeout=3)
|
||||||
|
if blank_ele:
|
||||||
|
try: blank_ele.click()
|
||||||
|
except: page.run_js("arguments[0].click();", blank_ele)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
log("INFO", f"开启底层数据拦截网: {API_TARGET}")
|
||||||
|
page.listen.start(API_TARGET)
|
||||||
|
|
||||||
|
packet = page.listen.wait(timeout=10)
|
||||||
|
if not packet:
|
||||||
|
query_btn_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[2]/div[1]/div[1]/div/button[1]/span'
|
||||||
|
query_btn = page.ele(query_btn_xpath, timeout=3)
|
||||||
|
if query_btn:
|
||||||
|
try: query_btn.click()
|
||||||
|
except: page.run_js("arguments[0].click();", query_btn)
|
||||||
|
packet = page.listen.wait(timeout=15)
|
||||||
|
|
||||||
|
if not packet:
|
||||||
|
log("ERR", "未能拦截到第一页数据,无法获取线上总条数。")
|
||||||
|
return
|
||||||
|
|
||||||
|
body = packet.response.body
|
||||||
|
data = body if isinstance(body, (dict, list)) else json.loads(body)
|
||||||
|
|
||||||
|
remote_count = 0
|
||||||
|
if isinstance(data, dict) and "result" in data:
|
||||||
|
remote_count = data["result"].get("totalCount", 0)
|
||||||
|
|
||||||
|
log("INFO", f"🌐 线上 ERP 系统当前总条数: {remote_count} 条")
|
||||||
|
|
||||||
|
if remote_count == local_count:
|
||||||
|
log("OK", "🎉 线上条数与本地一致,数据已是最新状态,无需抓取!")
|
||||||
|
return
|
||||||
|
|
||||||
|
new_items_count = remote_count - local_count
|
||||||
|
if new_items_count > 0:
|
||||||
|
log("INFO", f"🔥 发现大致 {new_items_count} 条新增数据!准备从第 1 页开始扫描录入...")
|
||||||
|
else:
|
||||||
|
log("INFO", f"⚠️ 线上条数 ({remote_count}) 少于本地条数 ({local_count}),可能存在数据删除。仍将扫描第一页验证更新。")
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# 开始处理第一页,并循环往后翻,直到遇到重复数据
|
||||||
|
# =========================================================
|
||||||
|
current_page = 1
|
||||||
|
cursor = conn.cursor()
|
||||||
|
total_inserted = 0
|
||||||
|
|
||||||
|
# 第一次的数据已经在上面的 packet 里了,直接处理
|
||||||
|
first_page_data = data
|
||||||
|
|
||||||
|
while True:
|
||||||
|
should_stop = False
|
||||||
|
inserted_this_page = 0
|
||||||
|
|
||||||
|
if isinstance(first_page_data, dict) and "result" in first_page_data:
|
||||||
|
items = first_page_data["result"].get("items", [])
|
||||||
|
if not items:
|
||||||
|
log("WARN", f"第 {current_page} 页返回了空列表,已无数据。")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 打印第一条数据的信息,用于调试
|
||||||
|
if items:
|
||||||
|
first_item = items[0]
|
||||||
|
log("INFO", f"🔍 正在检查本页第一条数据: 发料单 {first_item.get('workOrdersNumber')} 行号 {first_item.get('lineNumber')} 物料 {first_item.get('materialCode')}")
|
||||||
|
|
||||||
|
for raw_item in items:
|
||||||
|
# 1. 检查是否存在
|
||||||
|
if item_exists(cursor, raw_item):
|
||||||
|
# 发料单的新数据都在最前面。当我们遇到一条已经在数据库里的数据时,
|
||||||
|
# 说明这之前的数据都是新的,这之后的数据肯定都抓过了,直接停止。
|
||||||
|
log("INFO", f"🛑 在第 {current_page} 页发现本地已存在的记录 (发料单: {raw_item.get('workOrdersNumber')} 行号: {raw_item.get('lineNumber')} 物料: {raw_item.get('materialCode')}),增量扫描结束!")
|
||||||
|
should_stop = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# 2. 如果不存在,提取并插入
|
||||||
|
item = _extract_fields(raw_item)
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO issue_receipt_details (
|
||||||
|
production_order_no, product_material_code, product_material_name, product_material_specification,
|
||||||
|
work_orders_number, status, material_specification, material_name, material_code,
|
||||||
|
issue_number, has_issue_number, amount, cost_price, issue_amount,
|
||||||
|
production_order_remark, detailed_remark, unit_name, warehouse_name, line_number,
|
||||||
|
work_orders_remark, executor_user_name, material_model, execution_time, materials_user_name,
|
||||||
|
product_material_model, custom_field, department_information_code, department_information_name,
|
||||||
|
image_file, issue_amount_total, material_group_code, material_group_name,
|
||||||
|
numnber_of_reserved_digits, place_ment_strategy, price, sales_order_code
|
||||||
|
) VALUES (
|
||||||
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||||
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||||
|
)
|
||||||
|
''', (
|
||||||
|
item.get("生产任务单号"), item.get("生产物料代码"), item.get("生产物料名称"), item.get("生产物料规格"),
|
||||||
|
item.get("发料单号"), item.get("状态"), item.get("物料规格"), item.get("物料名称"), item.get("物料代码"),
|
||||||
|
item.get("发料数量"), item.get("已发料数量"), item.get("金额"), item.get("成本价"), item.get("发料金额"),
|
||||||
|
item.get("生产订单备注"), item.get("明细备注"), item.get("单位名称"), item.get("仓库名称"), item.get("行号"),
|
||||||
|
item.get("发料单备注"), item.get("执行人名称"), item.get("物料型号"), item.get("执行时间"), item.get("领料人"),
|
||||||
|
item.get("生产物料型号"), item.get("自定义字段"), item.get("部门代码"), item.get("部门名称"),
|
||||||
|
item.get("图片文件"), item.get("汇总金额"), item.get("物料组代码"), item.get("物料组名称"),
|
||||||
|
item.get("单价小数位数"), item.get("单价进位策略"), item.get("单价"), item.get("销售订单号")
|
||||||
|
))
|
||||||
|
inserted_this_page += 1
|
||||||
|
total_inserted += 1
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
log("OK", f"第 {current_page} 页处理完毕,成功插入 {inserted_this_page} 条新数据。")
|
||||||
|
|
||||||
|
if should_stop:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
log("ERR", f"第 {current_page} 页数据结构异常,中止。")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 如果没遇到旧数据,继续点击下一页
|
||||||
|
delay = random.uniform(1.5, 3.5)
|
||||||
|
log("INFO", f"⏳ 停顿 {delay:.2f} 秒后点击下一页...")
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
next_btn = None
|
||||||
|
for _ in range(3):
|
||||||
|
next_btn = page.ele('xpath://button[contains(@class, "btn-next")]', timeout=3)
|
||||||
|
if next_btn:
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if not next_btn:
|
||||||
|
next_btn = page.ele('xpath://i[contains(@class, "el-icon-arrow-right")]/parent::button', timeout=3)
|
||||||
|
|
||||||
|
if next_btn:
|
||||||
|
# 检查按钮是否被禁用
|
||||||
|
class_str = str(next_btn.attr("class"))
|
||||||
|
aria_disabled = next_btn.attr("aria-disabled")
|
||||||
|
is_disabled_attr = next_btn.attr("disabled") is not None
|
||||||
|
|
||||||
|
if "disabled" in class_str or is_disabled_attr or aria_disabled == "true":
|
||||||
|
log("OK", "🏁 下一页按钮已被禁用,已经翻到最后一页。")
|
||||||
|
break
|
||||||
|
|
||||||
|
try: next_btn.click()
|
||||||
|
except: page.run_js("arguments[0].click();", next_btn)
|
||||||
|
|
||||||
|
packet = page.listen.wait(timeout=15)
|
||||||
|
if not packet:
|
||||||
|
log("ERR", f"第 {current_page + 1} 页请求超时!")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 为下一轮循环准备数据
|
||||||
|
body = packet.response.body
|
||||||
|
first_page_data = body if isinstance(body, (dict, list)) else json.loads(body)
|
||||||
|
else:
|
||||||
|
log("ERR", "重试 3 次后仍然找不到下一页按钮!")
|
||||||
|
break
|
||||||
|
|
||||||
|
current_page += 1
|
||||||
|
|
||||||
|
log("OK", f"🎉 发料单增量同步大功告成!总计新增了 {total_inserted} 条记录入库!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log("ERR", f"发生全局异常: {e}")
|
||||||
|
finally:
|
||||||
|
if 'conn' in locals() and conn:
|
||||||
|
conn.close()
|
||||||
|
if 'page' in locals() and page:
|
||||||
|
try:
|
||||||
|
page.listen.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fetch_issue_receipt_incremental()
|
||||||
107
browser_login/fetch_work_orders.py
Normal file
107
browser_login/fetch_work_orders.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
"""
|
||||||
|
生产工单查询 - 数据提取
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page, login, login_manual, log, dump_page_state
|
||||||
|
from config import OUTPUT_DIR
|
||||||
|
|
||||||
|
PAGE_URL = "https://yunmes.tftykj.cn/WorkOrdersQuery"
|
||||||
|
API_PATH = "WorkOrdersDetailed_SearchListAll_Proxy" # 精确匹配生产工单列表 API
|
||||||
|
|
||||||
|
# ── 导航到目标页面 ───────────────────────────────────────────────────────────
|
||||||
|
def navigate_to_page(page):
|
||||||
|
log("INFO", f"导航到生产工单查询页面...")
|
||||||
|
page.get(PAGE_URL)
|
||||||
|
# 等待数据表格区域出现
|
||||||
|
table = page.ele("xpath://table | .el-table__body", timeout=15)
|
||||||
|
if table:
|
||||||
|
log("OK", "页面已加载")
|
||||||
|
else:
|
||||||
|
log("WARN", "表格元素未找到,继续执行")
|
||||||
|
|
||||||
|
# ── 拦截并获取第 1 页数据 ───────────────────────────────────────────────
|
||||||
|
def fetch_page1(page) -> dict:
|
||||||
|
log("INFO", "开启网络监听...")
|
||||||
|
page.listen.start(API_PATH)
|
||||||
|
|
||||||
|
# 刷新页面触发自动加载请求
|
||||||
|
page.refresh()
|
||||||
|
|
||||||
|
log("INFO", "等待 API 响应...")
|
||||||
|
packet = page.listen.wait(timeout=30)
|
||||||
|
page.listen.stop()
|
||||||
|
|
||||||
|
if not packet:
|
||||||
|
log("ERR", "超时未收到响应")
|
||||||
|
dump_page_state(page, "监听超时")
|
||||||
|
return None
|
||||||
|
|
||||||
|
log("OK", f"拦截成功 → HTTP {packet.response.status} (URL: {packet.request.url})")
|
||||||
|
|
||||||
|
try:
|
||||||
|
body = packet.response.body
|
||||||
|
data = body if isinstance(body, (dict, list)) else json.loads(body)
|
||||||
|
|
||||||
|
if isinstance(data, dict):
|
||||||
|
result = data.get("result", {})
|
||||||
|
if isinstance(result, dict) and "totalCount" in result:
|
||||||
|
items = result.get("items", [])
|
||||||
|
total = result.get("totalCount", "?")
|
||||||
|
log("INFO", f"本页记录: {len(items)} 条 | 总计: {total} 条")
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
log("WARN", f"解析 {packet.request.url} 失败: {e}")
|
||||||
|
|
||||||
|
log("ERR", "未解析到有效的数据结构")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ── 保存为 JSON ───────────────────────────────────────────────────────────────
|
||||||
|
def save_json(data, prefix: str = "work_orders") -> Path:
|
||||||
|
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
path = OUTPUT_DIR / f"{prefix}_{ts}.json"
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
log("OK", f"已保存: {path}")
|
||||||
|
return path
|
||||||
|
|
||||||
|
# ── 主流程 ────────────────────────────────────────────────────────────────────
|
||||||
|
def run(manual: bool = False):
|
||||||
|
mode = "复用保活浏览器"
|
||||||
|
log("INFO", f"生产工单查询提取启动 [{mode}]")
|
||||||
|
|
||||||
|
# 使用已经登录的保活浏览器端口 9222
|
||||||
|
page = get_page(port=9222)
|
||||||
|
try:
|
||||||
|
# Step 1: 进入页面
|
||||||
|
navigate_to_page(page)
|
||||||
|
|
||||||
|
# Step 2: 获取第 1 页数据
|
||||||
|
data = fetch_page1(page)
|
||||||
|
if data is None:
|
||||||
|
log("ERR", "数据获取失败")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Step 3: 保存 JSON
|
||||||
|
save_json(data, prefix="work_orders_page1")
|
||||||
|
log("OK", "完成!文件保存在 output/")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log("INFO", "用户中断 (Ctrl+C)")
|
||||||
|
except Exception as e:
|
||||||
|
log("ERR", f"异常: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
# 复用浏览器的情况下不关闭浏览器
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run(manual="--manual" in sys.argv)
|
||||||
241
browser_login/fetch_work_orders_incremental.py
Normal file
241
browser_login/fetch_work_orders_incremental.py
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
"""
|
||||||
|
生产工单报表 - 智能增量同步脚本
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page, log
|
||||||
|
from config import DB_PATH
|
||||||
|
from import_to_sqlite import init_db
|
||||||
|
|
||||||
|
HOME_URL = "https://yunmes.tftykj.cn/"
|
||||||
|
PAGE_URL = "https://yunmes.tftykj.cn/WorkOrdersQuery"
|
||||||
|
API_TARGET = "WorkOrdersDetailed_SearchListAll_Proxy"
|
||||||
|
|
||||||
|
def get_local_count(conn):
|
||||||
|
"""获取本地数据库已有的总记录数"""
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM work_orders")
|
||||||
|
return cursor.fetchone()[0]
|
||||||
|
|
||||||
|
def fetch_work_orders_incremental():
|
||||||
|
log("INFO", "=== 🚀 启动生产工单查询 - 智能增量同步 ===")
|
||||||
|
|
||||||
|
# 确保表结构已初始化
|
||||||
|
conn = init_db()
|
||||||
|
local_count = get_local_count(conn)
|
||||||
|
log("INFO", f"📦 本地数据库当前总计: {local_count} 条数据")
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
|
||||||
|
try:
|
||||||
|
log("INFO", f"正在进入工单页面: {PAGE_URL}")
|
||||||
|
page.get(PAGE_URL)
|
||||||
|
table = page.ele("xpath://table | .el-table__body", timeout=10)
|
||||||
|
if not table:
|
||||||
|
log("WARN", "未加载出工单页面表格元素,继续尝试监听...")
|
||||||
|
|
||||||
|
# 添加一小段硬延时,确保页面 JS 完全执行完毕
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
log("INFO", f"开启底层数据拦截网: {API_TARGET}")
|
||||||
|
page.listen.start(API_TARGET)
|
||||||
|
|
||||||
|
# 点击查询按钮触发第一页请求
|
||||||
|
query_btn = page.ele('#Search', timeout=3)
|
||||||
|
if not query_btn:
|
||||||
|
# 兼容 ElementUI 的按钮
|
||||||
|
query_btn_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[2]/div[1]/div[1]/div/button[1]/span'
|
||||||
|
query_btn = page.ele(query_btn_xpath, timeout=3)
|
||||||
|
|
||||||
|
if query_btn:
|
||||||
|
try: query_btn.click()
|
||||||
|
except: page.run_js("arguments[0].click();", query_btn)
|
||||||
|
|
||||||
|
packet = page.listen.wait(timeout=15)
|
||||||
|
if not packet:
|
||||||
|
# 备用方案:刷新页面
|
||||||
|
page.refresh()
|
||||||
|
packet = page.listen.wait(timeout=15)
|
||||||
|
|
||||||
|
if not packet:
|
||||||
|
log("ERR", "未能拦截到第一页数据,无法获取线上总条数。")
|
||||||
|
return
|
||||||
|
|
||||||
|
body = packet.response.body
|
||||||
|
data = body if isinstance(body, (dict, list)) else json.loads(body)
|
||||||
|
|
||||||
|
remote_count = 0
|
||||||
|
if isinstance(data, dict) and "result" in data:
|
||||||
|
result = data["result"]
|
||||||
|
if isinstance(result, dict):
|
||||||
|
remote_count = result.get("totalCount", 0)
|
||||||
|
|
||||||
|
log("INFO", f"🌐 线上 ERP 系统当前总条数: {remote_count} 条")
|
||||||
|
|
||||||
|
if remote_count <= local_count:
|
||||||
|
log("INFO", f"本地已有 {local_count} 条数据,但根据策略,我们将强制进行一轮全量更新检查...")
|
||||||
|
|
||||||
|
log("INFO", f"🔥 准备进行全量跳页抓取...")
|
||||||
|
|
||||||
|
# --- 【增量抓取策略优化】:不再根据总量做分页跳转 ---
|
||||||
|
# 始终从第 1 页(即最新发生变化/新增的工单页)开始抓取,
|
||||||
|
# 并往后翻页,直到发现连续 N 页的数据在本地数据库中都已经存在,即认为“增量部分”已抓取完毕。
|
||||||
|
start_page = 1
|
||||||
|
end_page = math.ceil(remote_count / 50)
|
||||||
|
|
||||||
|
log("INFO", f"🎯 增量抓取策略启动:从第 {start_page} 页向后抓取,直至遇到全为已存旧数据的页面。")
|
||||||
|
|
||||||
|
current_page = start_page
|
||||||
|
cursor = conn.cursor()
|
||||||
|
total_inserted = 0
|
||||||
|
total_updated = 0
|
||||||
|
consecutive_old_pages = 0 # 连续多少页都是老数据
|
||||||
|
|
||||||
|
while current_page <= end_page:
|
||||||
|
body = packet.response.body
|
||||||
|
data = body if isinstance(body, (dict, list)) else json.loads(body)
|
||||||
|
|
||||||
|
inserted_this_page = 0
|
||||||
|
if isinstance(data, dict) and "result" in data:
|
||||||
|
result = data.get("result", {})
|
||||||
|
if isinstance(result, dict):
|
||||||
|
items = result.get("items", [])
|
||||||
|
|
||||||
|
page_inserted = 0
|
||||||
|
page_updated = 0
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
wo_number = item.get("workOrdersNumber")
|
||||||
|
line_no = item.get("lineNumber")
|
||||||
|
mat_code = item.get("materialCode")
|
||||||
|
|
||||||
|
if not wo_number or not mat_code:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 检查此记录在本地是否已存在,以及关键状态是否发生变化
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT status, total_issue_number FROM work_orders
|
||||||
|
WHERE work_orders_number = ? AND line_number = ? AND material_code = ?
|
||||||
|
""", (wo_number, line_no, mat_code))
|
||||||
|
existing_record = cursor.fetchone()
|
||||||
|
|
||||||
|
new_status = item.get("status")
|
||||||
|
new_total_issue_number = item.get("hasIssueNumber")
|
||||||
|
|
||||||
|
if not existing_record:
|
||||||
|
# 本地不存在,执行插入
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO work_orders (
|
||||||
|
work_orders_number, line_number, material_code, material_name, material_specification,
|
||||||
|
status, unit_name, cost_price, issue_number, total_issue_number,
|
||||||
|
issue_amount, issue_amount_total, executor_user_name, execution_time,
|
||||||
|
production_order_no, warehouse_name, materials_user_name, work_orders_remark
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
''', (
|
||||||
|
wo_number, line_no, mat_code, item.get("materialName"), item.get("materialSpecification"),
|
||||||
|
new_status, item.get("unitName"), item.get("costPrice"), item.get("issueNumber"),
|
||||||
|
new_total_issue_number, item.get("issueAmount"), item.get("issueAmountTotal"),
|
||||||
|
item.get("executorUserName"), item.get("executionTime"), item.get("productionOrderNo"),
|
||||||
|
item.get("warehouseName"), item.get("materialsUserName"), item.get("workOrdersRemark")
|
||||||
|
))
|
||||||
|
page_inserted += 1
|
||||||
|
total_inserted += 1
|
||||||
|
else:
|
||||||
|
# 本地已存在,检查关键状态或数量是否有更新
|
||||||
|
old_status = existing_record[0]
|
||||||
|
old_total_issue_number = existing_record[1]
|
||||||
|
|
||||||
|
if str(old_status) != str(new_status) or str(old_total_issue_number) != str(new_total_issue_number):
|
||||||
|
cursor.execute('''
|
||||||
|
UPDATE work_orders SET
|
||||||
|
status = ?,
|
||||||
|
cost_price = ?,
|
||||||
|
issue_number = ?,
|
||||||
|
total_issue_number = ?,
|
||||||
|
issue_amount = ?,
|
||||||
|
issue_amount_total = ?,
|
||||||
|
executor_user_name = ?,
|
||||||
|
execution_time = ?,
|
||||||
|
warehouse_name = ?,
|
||||||
|
materials_user_name = ?
|
||||||
|
WHERE work_orders_number = ? AND line_number = ? AND material_code = ?
|
||||||
|
''', (
|
||||||
|
new_status, item.get("costPrice"), item.get("issueNumber"), new_total_issue_number,
|
||||||
|
item.get("issueAmount"), item.get("issueAmountTotal"), item.get("executorUserName"),
|
||||||
|
item.get("executionTime"), item.get("warehouseName"), item.get("materialsUserName"),
|
||||||
|
wo_number, line_no, mat_code
|
||||||
|
))
|
||||||
|
page_updated += 1
|
||||||
|
total_updated += 1
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
log("OK", f"第 {current_page} 页处理完毕: 新增 {page_inserted} 条, 更新 {page_updated} 条。")
|
||||||
|
|
||||||
|
# 增量判定逻辑:如果当前页全部都在本地存在,且没有任何一条发生了状态/数量的更新
|
||||||
|
# 则说明我们已经追溯到了历史旧数据,不需要再继续往后翻页抓取了!
|
||||||
|
if page_inserted == 0 and page_updated == 0:
|
||||||
|
consecutive_old_pages += 1
|
||||||
|
log("INFO", f"⚡️ 第 {current_page} 页全为无变动的旧数据 (累计 {consecutive_old_pages} 页)")
|
||||||
|
|
||||||
|
if consecutive_old_pages >= 2:
|
||||||
|
log("OK", "🎉 连续 2 页未发现新数据或变动数据,增量抓取完成,提前结束!")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# 只要有任何一条插入或更新,重置计数器
|
||||||
|
consecutive_old_pages = 0
|
||||||
|
|
||||||
|
if current_page < end_page:
|
||||||
|
delay = random.uniform(1.5, 3.5)
|
||||||
|
log("INFO", f"⏳ 停顿 {delay:.2f} 秒后点击下一页...")
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
next_btn = None
|
||||||
|
for _ in range(3):
|
||||||
|
# 尝试 EasyUI 的下一页按钮
|
||||||
|
next_btn = page.ele('xpath://*[contains(@class, "pagination-next")]/ancestor::a', timeout=3)
|
||||||
|
if not next_btn:
|
||||||
|
# 兼容 ElementUI 的下一页按钮
|
||||||
|
next_btn = page.ele('xpath://button[contains(@class, "btn-next")]', timeout=3)
|
||||||
|
|
||||||
|
if next_btn:
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if next_btn:
|
||||||
|
try: next_btn.click()
|
||||||
|
except: page.run_js("arguments[0].click();", next_btn)
|
||||||
|
|
||||||
|
packet = page.listen.wait(timeout=15)
|
||||||
|
if not packet:
|
||||||
|
log("ERR", f"第 {current_page + 1} 页请求超时!")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
log("ERR", "重试 3 次后仍然找不到下一页按钮!")
|
||||||
|
break
|
||||||
|
|
||||||
|
current_page += 1
|
||||||
|
|
||||||
|
log("OK", f"🎉 增量同步大功告成!总计向数据库执行了 {total_inserted} 次插入/更新操作!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log("ERR", f"发生全局异常: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
if 'conn' in locals() and conn:
|
||||||
|
conn.close()
|
||||||
|
if 'page' in locals() and page:
|
||||||
|
try:
|
||||||
|
page.listen.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fetch_work_orders_incremental()
|
||||||
13
browser_login/find_buttons.py
Normal file
13
browser_login/find_buttons.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
print("URL:", page.url)
|
||||||
|
|
||||||
|
links = page.eles('tag:a')
|
||||||
|
for a in links:
|
||||||
|
if a.text and len(a.text) < 10:
|
||||||
|
print("Link:", a.text, a.html)
|
||||||
|
|
||||||
12
browser_login/find_report.py
Normal file
12
browser_login/find_report.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
print("当前 URL:", page.url)
|
||||||
|
|
||||||
|
menus = page.eles('text:自定义报表')
|
||||||
|
for m in menus:
|
||||||
|
print("Found menu:", m.html)
|
||||||
|
|
||||||
17
browser_login/import_abnormal.py
Normal file
17
browser_login/import_abnormal.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from import_to_sqlite import import_abnormal_report_data
|
||||||
|
from config import OUTPUT_DIR
|
||||||
|
|
||||||
|
latest_file = OUTPUT_DIR / "report_abnormal_20260611_120355.json"
|
||||||
|
print(f"Importing from {latest_file}")
|
||||||
|
with open(latest_file, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
items = data.get('result', {}).get('items', [])
|
||||||
|
if items:
|
||||||
|
count = import_abnormal_report_data(items)
|
||||||
|
print(f"Imported {count} items.")
|
||||||
|
else:
|
||||||
|
print("No items in json.")
|
||||||
@@ -6,6 +6,7 @@ from config import OUTPUT_DIR, DB_PATH
|
|||||||
|
|
||||||
RECEIPT_JSON = OUTPUT_DIR / "receipt_details_full_clean.json"
|
RECEIPT_JSON = OUTPUT_DIR / "receipt_details_full_clean.json"
|
||||||
BOM_JSON = OUTPUT_DIR / "bom_cost_full_tree_final.json"
|
BOM_JSON = OUTPUT_DIR / "bom_cost_full_tree_final.json"
|
||||||
|
ISSUE_JSON = OUTPUT_DIR / "issue_receipt_details_full.json"
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
"""初始化数据库并创建表"""
|
"""初始化数据库并创建表"""
|
||||||
@@ -40,6 +41,68 @@ def init_db():
|
|||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_receipt_supplier_name ON receipt_details(supplier_name)')
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_receipt_supplier_name ON receipt_details(supplier_name)')
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_receipt_time ON receipt_details(receipt_time)')
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_receipt_time ON receipt_details(receipt_time)')
|
||||||
|
|
||||||
|
# 创建发料明细表
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS issue_receipt_details (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
production_order_no TEXT,
|
||||||
|
product_material_code TEXT,
|
||||||
|
product_material_name TEXT,
|
||||||
|
product_material_specification TEXT,
|
||||||
|
work_orders_number TEXT,
|
||||||
|
status INTEGER,
|
||||||
|
material_specification TEXT,
|
||||||
|
material_name TEXT,
|
||||||
|
material_code TEXT,
|
||||||
|
issue_number REAL,
|
||||||
|
has_issue_number REAL,
|
||||||
|
amount REAL,
|
||||||
|
cost_price REAL,
|
||||||
|
issue_amount REAL,
|
||||||
|
production_order_remark TEXT,
|
||||||
|
detailed_remark TEXT,
|
||||||
|
unit_name TEXT,
|
||||||
|
warehouse_name TEXT,
|
||||||
|
line_number INTEGER,
|
||||||
|
work_orders_remark TEXT,
|
||||||
|
executor_user_name TEXT,
|
||||||
|
material_model TEXT,
|
||||||
|
execution_time TEXT,
|
||||||
|
materials_user_name TEXT,
|
||||||
|
product_material_model TEXT,
|
||||||
|
custom_field TEXT,
|
||||||
|
department_information_code TEXT,
|
||||||
|
department_information_name TEXT,
|
||||||
|
image_file TEXT,
|
||||||
|
issue_amount_total REAL,
|
||||||
|
material_group_code TEXT,
|
||||||
|
material_group_name TEXT,
|
||||||
|
numnber_of_reserved_digits INTEGER,
|
||||||
|
place_ment_strategy INTEGER,
|
||||||
|
price REAL,
|
||||||
|
sales_order_code TEXT
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_issue_material_code ON issue_receipt_details(material_code)')
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_issue_execution_time ON issue_receipt_details(execution_time)')
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_issue_work_orders_number ON issue_receipt_details(work_orders_number)')
|
||||||
|
|
||||||
|
# 删除历史可能存在的重复数据,确保唯一索引能够成功创建
|
||||||
|
cursor.execute('''
|
||||||
|
DELETE FROM issue_receipt_details
|
||||||
|
WHERE id NOT IN (
|
||||||
|
SELECT MIN(id)
|
||||||
|
FROM issue_receipt_details
|
||||||
|
GROUP BY work_orders_number, line_number, material_code
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 为发料明细表创建唯一索引,用于 UPSERT 冲突检测
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_issue_unique
|
||||||
|
ON issue_receipt_details(work_orders_number, line_number, material_code)
|
||||||
|
''')
|
||||||
|
|
||||||
# 注意:为了在打包部署时不丢失用户已抓取的数据,改为 IF NOT EXISTS
|
# 注意:为了在打包部署时不丢失用户已抓取的数据,改为 IF NOT EXISTS
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS bom_parent (
|
CREATE TABLE IF NOT EXISTS bom_parent (
|
||||||
@@ -66,6 +129,81 @@ def init_db():
|
|||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_bom_child_parent_code ON bom_child(parent_material_code)')
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_bom_child_parent_code ON bom_child(parent_material_code)')
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_bom_child_node_code ON bom_child(node_material_code)')
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_bom_child_node_code ON bom_child(node_material_code)')
|
||||||
|
|
||||||
|
# 创建生产工单表
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS work_orders (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
work_orders_number TEXT,
|
||||||
|
line_number INTEGER,
|
||||||
|
material_code TEXT,
|
||||||
|
material_name TEXT,
|
||||||
|
material_specification TEXT,
|
||||||
|
status INTEGER,
|
||||||
|
unit_name TEXT,
|
||||||
|
cost_price REAL,
|
||||||
|
issue_number REAL,
|
||||||
|
total_issue_number REAL,
|
||||||
|
issue_amount REAL,
|
||||||
|
issue_amount_total REAL,
|
||||||
|
executor_user_name TEXT,
|
||||||
|
execution_time TEXT,
|
||||||
|
production_order_no TEXT,
|
||||||
|
warehouse_name TEXT,
|
||||||
|
materials_user_name TEXT,
|
||||||
|
work_orders_remark TEXT
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_work_orders_number ON work_orders(work_orders_number)')
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_work_orders_material_code ON work_orders(material_code)')
|
||||||
|
|
||||||
|
# 删除可能存在的重复数据
|
||||||
|
cursor.execute('''
|
||||||
|
DELETE FROM work_orders
|
||||||
|
WHERE id NOT IN (
|
||||||
|
SELECT MIN(id)
|
||||||
|
FROM work_orders
|
||||||
|
GROUP BY work_orders_number, line_number, material_code
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 唯一索引
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_work_orders_unique
|
||||||
|
ON work_orders(work_orders_number, line_number, material_code)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 创建发料异常报表表
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS abnormal_report (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
work_orders_number TEXT,
|
||||||
|
product_code TEXT,
|
||||||
|
product_name TEXT,
|
||||||
|
status TEXT,
|
||||||
|
completed_qty REAL,
|
||||||
|
order_date TEXT,
|
||||||
|
workshop TEXT,
|
||||||
|
material_code TEXT,
|
||||||
|
material_name TEXT,
|
||||||
|
material_specification TEXT,
|
||||||
|
unit_qty REAL,
|
||||||
|
total_demand_qty REAL,
|
||||||
|
warehouse_issue_qty REAL,
|
||||||
|
theoretical_issue_qty REAL,
|
||||||
|
issue_method TEXT,
|
||||||
|
issue_status TEXT
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_abnormal_work_orders_number ON abnormal_report(work_orders_number)')
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_abnormal_material_code ON abnormal_report(material_code)')
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_abnormal_unique
|
||||||
|
ON abnormal_report(work_orders_number, material_code)
|
||||||
|
''')
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
@@ -80,68 +218,74 @@ def import_receipt_details(conn):
|
|||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
# 清空旧数据(如果需要重复运行),并且我们现在要更新表结构
|
|
||||||
cursor.execute('DROP TABLE IF EXISTS receipt_details')
|
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE receipt_details (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
purchase_order_code TEXT,
|
|
||||||
row_no INTEGER,
|
|
||||||
material_code TEXT,
|
|
||||||
material_name TEXT,
|
|
||||||
material_specification TEXT,
|
|
||||||
warehouse_code TEXT,
|
|
||||||
warehouse_name TEXT,
|
|
||||||
supplier_code TEXT,
|
|
||||||
supplier_name TEXT,
|
|
||||||
unit_name TEXT,
|
|
||||||
conversion_unit TEXT,
|
|
||||||
receive_price REAL,
|
|
||||||
receipt_time TEXT,
|
|
||||||
purchase_qty REAL,
|
|
||||||
receive_qty REAL,
|
|
||||||
total_amount REAL
|
|
||||||
)
|
|
||||||
''')
|
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_receipt_material_code ON receipt_details(material_code)')
|
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_receipt_supplier_name ON receipt_details(supplier_name)')
|
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_receipt_time ON receipt_details(receipt_time)')
|
|
||||||
|
|
||||||
count = 0
|
# 删除历史可能存在的重复数据,确保唯一索引能够成功创建
|
||||||
|
cursor.execute('''
|
||||||
|
DELETE FROM receipt_details
|
||||||
|
WHERE id NOT IN (
|
||||||
|
SELECT MIN(id)
|
||||||
|
FROM receipt_details
|
||||||
|
GROUP BY purchase_order_code, row_no, material_code
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 为了避免没有唯一索引导致 UPSERT 报错,这里显式创建一次
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_receipt_unique
|
||||||
|
ON receipt_details(purchase_order_code, row_no, material_code)
|
||||||
|
''')
|
||||||
|
|
||||||
|
count_inserted = 0
|
||||||
|
|
||||||
for item in data:
|
for item in data:
|
||||||
p_qty = item.get("进货数量")
|
p_qty = item.get("进货数量")
|
||||||
r_qty = item.get("收货数量")
|
r_qty = item.get("收货数量")
|
||||||
|
po_code = item.get("采购订单号")
|
||||||
|
row_no = item.get("行号")
|
||||||
|
mat_code = item.get("物料代码")
|
||||||
|
|
||||||
cursor.execute('''
|
# 容错:如果关键字段为空则跳过
|
||||||
INSERT INTO receipt_details (
|
if not po_code or not row_no or not mat_code:
|
||||||
purchase_order_code, row_no, material_code, material_name,
|
continue
|
||||||
material_specification, warehouse_code, warehouse_name,
|
|
||||||
supplier_code, supplier_name, unit_name, conversion_unit,
|
try:
|
||||||
receive_price, receipt_time,
|
cursor.execute('''
|
||||||
purchase_qty, receive_qty, total_amount
|
INSERT INTO receipt_details (
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
purchase_order_code, row_no, material_code, material_name,
|
||||||
''', (
|
material_specification, warehouse_code, warehouse_name,
|
||||||
item.get("采购订单号"),
|
supplier_code, supplier_name, unit_name, conversion_unit,
|
||||||
item.get("行号"),
|
receive_price, receipt_time,
|
||||||
item.get("物料代码"),
|
purchase_qty, receive_qty, total_amount
|
||||||
item.get("物料名称"),
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
item.get("物料规格"),
|
ON CONFLICT(purchase_order_code, row_no, material_code) DO UPDATE SET
|
||||||
item.get("仓库代码"),
|
receive_price=excluded.receive_price,
|
||||||
item.get("仓库名称"),
|
receive_qty=excluded.receive_qty,
|
||||||
item.get("供应商代码"),
|
total_amount=excluded.total_amount,
|
||||||
item.get("供应商名称"),
|
receipt_time=excluded.receipt_time
|
||||||
item.get("单位名称"),
|
''', (
|
||||||
item.get("转换单位"),
|
po_code,
|
||||||
item.get("收货单价"),
|
row_no,
|
||||||
item.get("收货时间"),
|
mat_code,
|
||||||
p_qty,
|
item.get("物料名称"),
|
||||||
r_qty,
|
item.get("物料规格"),
|
||||||
item.get("收货总金额")
|
item.get("仓库代码"),
|
||||||
))
|
item.get("仓库名称"),
|
||||||
count += 1
|
item.get("供应商代码"),
|
||||||
|
item.get("供应商名称"),
|
||||||
|
item.get("单位名称"),
|
||||||
|
item.get("转换单位"),
|
||||||
|
item.get("收货单价"),
|
||||||
|
item.get("收货时间"),
|
||||||
|
p_qty,
|
||||||
|
r_qty,
|
||||||
|
item.get("收货总金额")
|
||||||
|
))
|
||||||
|
count_inserted += 1
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"入库报错 收货单:{po_code} 行号:{row_no} 错误:{e}")
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
print(f"成功导入 {count} 条收货明细数据!")
|
print(f"成功处理(新增或更新) {count_inserted} 条收货明细数据!")
|
||||||
|
|
||||||
def _insert_bom_tree(cursor, parent_material_code, tree_nodes, parent_node_id=None):
|
def _insert_bom_tree(cursor, parent_material_code, tree_nodes, parent_node_id=None):
|
||||||
"""递归插入 BOM 树节点"""
|
"""递归插入 BOM 树节点"""
|
||||||
@@ -213,6 +357,128 @@ def import_bom_data(conn):
|
|||||||
child_count = cursor.fetchone()[0]
|
child_count = cursor.fetchone()[0]
|
||||||
print(f"成功导入 {parent_count} 个 BOM 父件,包含 {child_count} 个子件节点!")
|
print(f"成功导入 {parent_count} 个 BOM 父件,包含 {child_count} 个子件节点!")
|
||||||
|
|
||||||
|
def import_issue_receipt_details(conn):
|
||||||
|
"""导入发料明细数据"""
|
||||||
|
if not ISSUE_JSON.exists():
|
||||||
|
print(f"找不到发料明细文件: {ISSUE_JSON}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("开始导入发料明细数据...")
|
||||||
|
with open(ISSUE_JSON, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 为了避免没有唯一索引导致 UPSERT 报错,这里显式创建一次
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_issue_unique
|
||||||
|
ON issue_receipt_details(work_orders_number, line_number, material_code)
|
||||||
|
''')
|
||||||
|
|
||||||
|
count_inserted = 0
|
||||||
|
count_updated = 0
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
# 提取关键字段
|
||||||
|
wo_number = item.get("发料单号")
|
||||||
|
line_no = item.get("行号")
|
||||||
|
mat_code = item.get("物料代码")
|
||||||
|
|
||||||
|
# 容错:如果关键字段为空则跳过
|
||||||
|
if not wo_number or not line_no or not mat_code:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO issue_receipt_details (
|
||||||
|
production_order_no, product_material_code, product_material_name, product_material_specification,
|
||||||
|
work_orders_number, status, material_specification, material_name, material_code,
|
||||||
|
issue_number, has_issue_number, amount, cost_price, issue_amount,
|
||||||
|
production_order_remark, detailed_remark, unit_name, warehouse_name, line_number,
|
||||||
|
work_orders_remark, executor_user_name, material_model, execution_time, materials_user_name,
|
||||||
|
product_material_model, custom_field, department_information_code, department_information_name,
|
||||||
|
image_file, issue_amount_total, material_group_code, material_group_name,
|
||||||
|
numnber_of_reserved_digits, place_ment_strategy, price, sales_order_code
|
||||||
|
) VALUES (
|
||||||
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||||
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||||
|
)
|
||||||
|
ON CONFLICT(work_orders_number, line_number, material_code) DO UPDATE SET
|
||||||
|
status=excluded.status,
|
||||||
|
issue_number=excluded.issue_number,
|
||||||
|
has_issue_number=excluded.has_issue_number,
|
||||||
|
amount=excluded.amount,
|
||||||
|
cost_price=excluded.cost_price,
|
||||||
|
issue_amount=excluded.issue_amount,
|
||||||
|
warehouse_name=excluded.warehouse_name,
|
||||||
|
executor_user_name=excluded.executor_user_name,
|
||||||
|
execution_time=excluded.execution_time,
|
||||||
|
materials_user_name=excluded.materials_user_name,
|
||||||
|
issue_amount_total=excluded.issue_amount_total
|
||||||
|
''', (
|
||||||
|
item.get("生产任务单号"), item.get("生产物料代码"), item.get("生产物料名称"), item.get("生产物料规格"),
|
||||||
|
wo_number, item.get("状态"), item.get("物料规格"), item.get("物料名称"), mat_code,
|
||||||
|
item.get("发料数量"), item.get("已发料数量"), item.get("金额"), item.get("成本价"), item.get("发料金额"),
|
||||||
|
item.get("生产订单备注"), item.get("明细备注"), item.get("单位名称"), item.get("仓库名称"), line_no,
|
||||||
|
item.get("发料单备注"), item.get("执行人名称"), item.get("物料型号"), item.get("执行时间"), item.get("领料人"),
|
||||||
|
item.get("生产物料型号"), item.get("自定义字段"), item.get("部门代码"), item.get("部门名称"),
|
||||||
|
item.get("图片文件"), item.get("汇总金额"), item.get("物料组代码"), item.get("物料组名称"),
|
||||||
|
item.get("单价小数位数"), item.get("单价进位策略"), item.get("单价"), item.get("销售订单号")
|
||||||
|
))
|
||||||
|
|
||||||
|
# 由于 sqlite3 在 UPSERT 时,如果发生了 UPDATE,rowcount 也是 1
|
||||||
|
# 但我们可以通过比较变化前后的总数来粗略估计,或者统一提示处理成功
|
||||||
|
count_inserted += 1
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"入库报错 发料单:{wo_number} 行号:{line_no} 错误:{e}")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print(f"成功处理(新增或更新) {count_inserted} 条发料明细数据!")
|
||||||
|
|
||||||
|
def import_abnormal_report_data(items):
|
||||||
|
"""直接将 API 获取到的异常报表 items 数组存入数据库"""
|
||||||
|
conn = init_db()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
count_inserted = 0
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
wo_number = item.get("生产工单号")
|
||||||
|
mat_code = item.get("需求物料代码")
|
||||||
|
|
||||||
|
if not wo_number or not mat_code:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO abnormal_report (
|
||||||
|
work_orders_number, product_code, product_name, status, completed_qty,
|
||||||
|
order_date, workshop, material_code, material_name, material_specification,
|
||||||
|
unit_qty, total_demand_qty, warehouse_issue_qty, theoretical_issue_qty,
|
||||||
|
issue_method, issue_status
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(work_orders_number, material_code) DO UPDATE SET
|
||||||
|
status=excluded.status,
|
||||||
|
completed_qty=excluded.completed_qty,
|
||||||
|
warehouse_issue_qty=excluded.warehouse_issue_qty,
|
||||||
|
theoretical_issue_qty=excluded.theoretical_issue_qty,
|
||||||
|
issue_status=excluded.issue_status
|
||||||
|
''', (
|
||||||
|
wo_number, item.get("产品代码"), item.get("产品名称"), item.get("工单状态"),
|
||||||
|
item.get("已完工数量"), item.get("下单日期"), item.get("生产车间"),
|
||||||
|
mat_code, item.get("需求物料名称"), item.get("需求物料规格"),
|
||||||
|
item.get("单机用量"), item.get("需求总量"), item.get("仓库发放数量"),
|
||||||
|
item.get("理论仓库出料数量"), item.get("发料方式"), item.get("发料情况")
|
||||||
|
))
|
||||||
|
count_inserted += 1
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"异常报表入库报错 工单:{wo_number} 物料:{mat_code} 错误:{e}")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return count_inserted
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import sys
|
import sys
|
||||||
print(f"数据库文件将保存在: {DB_PATH}")
|
print(f"数据库文件将保存在: {DB_PATH}")
|
||||||
@@ -225,10 +491,13 @@ if __name__ == "__main__":
|
|||||||
import_bom_data(conn)
|
import_bom_data(conn)
|
||||||
elif "--receipt-only" in args:
|
elif "--receipt-only" in args:
|
||||||
import_receipt_details(conn)
|
import_receipt_details(conn)
|
||||||
|
elif "--issue-only" in args:
|
||||||
|
import_issue_receipt_details(conn)
|
||||||
else:
|
else:
|
||||||
# 默认全量导入
|
# 默认全量导入
|
||||||
import_receipt_details(conn)
|
import_receipt_details(conn)
|
||||||
import_bom_data(conn)
|
import_bom_data(conn)
|
||||||
|
import_issue_receipt_details(conn)
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
print("全部导入完成!你可以使用 SQLite 客户端连接 erp_data.db 查看数据。")
|
print("全部导入完成!你可以使用 SQLite 客户端连接 erp_data.db 查看数据。")
|
||||||
18
browser_login/inspect_current.py
Normal file
18
browser_login/inspect_current.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
tabs = page.get_tabs()
|
||||||
|
print("当前打开的标签页:")
|
||||||
|
for t in tabs:
|
||||||
|
print(t.url)
|
||||||
|
|
||||||
|
target_tab = page.get_tab(page.latest_tab)
|
||||||
|
print("\n当前活动标签页 URL:", target_tab.url)
|
||||||
|
|
||||||
|
# 打印一下当前的弹窗或按钮
|
||||||
|
print("包含'查询'的元素:")
|
||||||
|
for el in target_tab.eles('text:查询'):
|
||||||
|
print(el.html)
|
||||||
17
browser_login/inspect_menus.py
Normal file
17
browser_login/inspect_menus.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
page.get("https://yunmes.tftykj.cn/")
|
||||||
|
|
||||||
|
print("等待页面加载...")
|
||||||
|
page.wait.load_start()
|
||||||
|
|
||||||
|
# 尝试寻找菜单
|
||||||
|
menus = page.eles('tag:div@@text():自定义报表管理')
|
||||||
|
print(f"找到 {len(menus)} 个 自定义报表管理")
|
||||||
|
for m in menus:
|
||||||
|
print(m.html)
|
||||||
|
|
||||||
41
browser_login/inspect_report.py
Normal file
41
browser_login/inspect_report.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
target_tab = page.get_tab(page.latest_tab)
|
||||||
|
|
||||||
|
target_tab.listen.start()
|
||||||
|
|
||||||
|
btn = target_tab.ele('#onSearch') or target_tab.ele('text=查询')
|
||||||
|
if btn:
|
||||||
|
btn.run_js('this.click()')
|
||||||
|
print("Clicked")
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
for p in target_tab.listen.steps():
|
||||||
|
if p.method == 'POST':
|
||||||
|
print("API:", p.url)
|
||||||
|
body = p.response.body
|
||||||
|
try:
|
||||||
|
data = body if isinstance(body, (dict, list)) else json.loads(body)
|
||||||
|
# dump each to a file for inspection
|
||||||
|
ts = time.time()
|
||||||
|
with open(f"browser_login/output/inspect_{ts}.json", "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
if isinstance(data, dict):
|
||||||
|
print("Keys:", list(data.keys()))
|
||||||
|
if 'result' in data:
|
||||||
|
res = data['result']
|
||||||
|
if isinstance(res, dict):
|
||||||
|
print("Result Keys:", list(res.keys()))
|
||||||
|
else:
|
||||||
|
print("Result is list/other")
|
||||||
|
elif 'rows' in data:
|
||||||
|
print("Rows length:", len(data['rows']))
|
||||||
|
except Exception as e:
|
||||||
|
print("Error parsing:", e)
|
||||||
43
browser_login/navigate_report.py
Normal file
43
browser_login/navigate_report.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
page.get("https://yunmes.tftykj.cn/")
|
||||||
|
page.wait.load_start()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("点击一级菜单: 自定义报表管理")
|
||||||
|
# 这里可能有多个匹配,找可见的那个
|
||||||
|
menu1 = page.ele('text=自定义报表管理', timeout=5)
|
||||||
|
if menu1:
|
||||||
|
menu1.click()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
print("点击二级菜单: 自定义报表管理")
|
||||||
|
menu2 = page.eles('text=自定义报表管理')[1] # 或者是下一个
|
||||||
|
if menu2:
|
||||||
|
menu2.click()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
print("点击三级菜单: 自定义报表")
|
||||||
|
menu3 = page.ele('text=自定义报表', timeout=5)
|
||||||
|
if menu3:
|
||||||
|
menu3.click()
|
||||||
|
time.sleep(2)
|
||||||
|
print("成功进?mport sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, stint("找不到三级菜?ys.path.insert(0, str( from login import get_page
|
||||||
|
|
||||||
|
page = get_page(p??page = get_pag表管理")
|
||||||
|
epage.get("https://yunmes. ppage.wait.load_start()
|
||||||
|
time.sleep(2)ontime.sleep(2)
|
||||||
|
|
||||||
|
try:
|
||||||
|
at
|
||||||
|
try:
|
||||||
|
y
|
||||||
19
browser_login/search_report.py
Normal file
19
browser_login/search_report.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
print("URL:", page.url)
|
||||||
|
|
||||||
|
ele = page.ele('text:生产工单发料异常检查报表')
|
||||||
|
if ele:
|
||||||
|
print("找到了报表:", ele.html)
|
||||||
|
else:
|
||||||
|
print("未找到报表,尝试在搜索框输入")
|
||||||
|
# 查找输入框,可能是 placeholder 为 "请输入报表名称" 等
|
||||||
|
inputs = page.eles('tag:input')
|
||||||
|
for i in inputs:
|
||||||
|
print(i.attr('placeholder'))
|
||||||
|
|
||||||
16
browser_login/test_date.py
Normal file
16
browser_login/test_date.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import sys, time
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page
|
||||||
|
|
||||||
|
page = get_page(port=9222)
|
||||||
|
target_tab = page.get_tab(page.latest_tab)
|
||||||
|
|
||||||
|
btn = target_tab.ele('#onSearch') or target_tab.ele('text=查询')
|
||||||
|
if btn:
|
||||||
|
print("Found btn:", btn.html)
|
||||||
|
try:
|
||||||
|
btn.click(by_js=True)
|
||||||
|
print("Click by_js success")
|
||||||
|
except Exception as e:
|
||||||
|
print("Error click:", e)
|
||||||
91
browser_login/test_fill_date.py
Normal file
91
browser_login/test_fill_date.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
"""
|
||||||
|
测试脚本:尝试在 ERP 质量报表页面填写“下单日期(开始)”
|
||||||
|
目标: 验证 DrissionPage 是否能成功清除并输入 ElementUI 的日期选择器。
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page, log
|
||||||
|
|
||||||
|
HOME_URL = "https://yunmes.tftykj.cn/"
|
||||||
|
|
||||||
|
def test_fill_date():
|
||||||
|
log("INFO", "=== 🧪 启动日期输入框填写测试 ===")
|
||||||
|
page = get_page(port=9222)
|
||||||
|
|
||||||
|
try:
|
||||||
|
log("INFO", f"正在回到主页起点: {HOME_URL}")
|
||||||
|
page.get(HOME_URL)
|
||||||
|
page.wait.load_start()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
menus = [
|
||||||
|
("进入质量报表", 'xpath://*[@id="el-collapse-content-21"]/div/div/div/div[1]/div/div/div[6]/div')
|
||||||
|
]
|
||||||
|
|
||||||
|
log("INFO", "开始模拟人工点击左侧导航菜单...")
|
||||||
|
for name, xpath in menus:
|
||||||
|
ele = page.ele(xpath, timeout=5)
|
||||||
|
if ele:
|
||||||
|
try: ele.click()
|
||||||
|
except: page.run_js("arguments[0].click();", ele)
|
||||||
|
else:
|
||||||
|
log("ERR", f"找不到菜单元素: {name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
log("OK", "✅ 成功点开质量报表界面!")
|
||||||
|
|
||||||
|
# 等待页面稍微加载一下
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# 尝试寻找并填写下单日期(开始)
|
||||||
|
date_xpath = 'xpath://*[@id="customTable-search-area"]/div[1]/div/div[2]/div[11]/div[2]/span/input[1]'
|
||||||
|
log("INFO", f"正在寻找日期输入框: {date_xpath}")
|
||||||
|
|
||||||
|
date_input = page.ele(date_xpath, timeout=5)
|
||||||
|
|
||||||
|
if date_input:
|
||||||
|
log("INFO", "✅ 找到输入框!尝试清除并输入 '2026-05-01'...")
|
||||||
|
|
||||||
|
# ElementUI 的日期输入框比较难搞,通常需要组合拳
|
||||||
|
# 放弃在 UI 层面折腾这个顽固的日期选择器
|
||||||
|
# 我们采用“黑客”做法:直接在浏览器底层拦截并篡改即将发出的网络请求数据包!
|
||||||
|
API_TARGET = "SearchCustomReportBySQL_Proxy"
|
||||||
|
|
||||||
|
# 1. 设置请求拦截器
|
||||||
|
log("INFO", f"正在开启全局请求拦截器,目标: {API_TARGET}")
|
||||||
|
page.listen.start(API_TARGET)
|
||||||
|
|
||||||
|
# 2. 我们不需要在输入框里填东西了,直接去点击查询按钮
|
||||||
|
query_btn_xpath = 'xpath://*[@id="customTable-search-area"]/div[1]/div/div[1]/a[2]/span/span'
|
||||||
|
query_btn = page.ele(query_btn_xpath, timeout=3)
|
||||||
|
|
||||||
|
if query_btn:
|
||||||
|
log("INFO", "准备点击【查询】按钮触发网络请求...")
|
||||||
|
try:
|
||||||
|
query_btn.click()
|
||||||
|
except:
|
||||||
|
page.run_js("arguments[0].click();", query_btn)
|
||||||
|
|
||||||
|
# 3. 拦截请求
|
||||||
|
packet = page.listen.wait(timeout=10)
|
||||||
|
if packet:
|
||||||
|
log("OK", "✅ 成功拦截到了查询请求!")
|
||||||
|
|
||||||
|
# 打印一下它原来发送的数据体,看看结构
|
||||||
|
raw_post_data = packet.request.postData
|
||||||
|
log("INFO", "原始发送的数据截断前100字符: " + str(raw_post_data)[:100])
|
||||||
|
|
||||||
|
log("OK", "如果这种拦截思路可行,我们下一步就可以在发送前篡改它里面的日期参数!")
|
||||||
|
else:
|
||||||
|
log("ERR", "未能拦截到查询请求,可能超时。")
|
||||||
|
else:
|
||||||
|
log("ERR", "未找到【查询】按钮。")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log("ERR", f"发生异常: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_fill_date()
|
||||||
87
browser_login/test_vue_injection.py
Normal file
87
browser_login/test_vue_injection.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
"""
|
||||||
|
测试强行注入 Vue 实例修改日期控件的值
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from login import get_page, log
|
||||||
|
|
||||||
|
HOME_URL = "https://yunmes.tftykj.cn/"
|
||||||
|
|
||||||
|
def test_vue_injection():
|
||||||
|
log("INFO", "=== 🧪 启动 Vue 实例强行注入测试 ===")
|
||||||
|
page = get_page(port=9222)
|
||||||
|
|
||||||
|
try:
|
||||||
|
log("INFO", "⚠️ 假设您已经在保活浏览器中手动打开了【质量报表】页面!")
|
||||||
|
|
||||||
|
# 尝试刷新一下页面,确保处于初始状态 (可选,这里先不刷新,直接找元素)
|
||||||
|
# page.refresh()
|
||||||
|
# page.wait.load_start()
|
||||||
|
# time.sleep(2)
|
||||||
|
|
||||||
|
date_xpath = 'xpath://*[@id="customTable-search-area"]/div[1]/div/div[2]/div[11]/div[2]/span/input[1]'
|
||||||
|
log("INFO", f"正在当前页面寻找日期输入框: {date_xpath}")
|
||||||
|
date_input = page.ele(date_xpath, timeout=5)
|
||||||
|
|
||||||
|
if date_input:
|
||||||
|
log("INFO", "✅ 找到输入框!准备执行 Vue 实例劫持注入...")
|
||||||
|
|
||||||
|
# 黑客核心:拿到 DOM 元素,获取其上的 __vue__ 实例,然后修改其父组件的 value 或 model 值
|
||||||
|
# ElementUI 的 el-date-picker 绑定的值通常在自身或其父组件实例上
|
||||||
|
# 这里尝试了多种可能的 Vue 内部变量名以确保万无一失
|
||||||
|
vue_hack_js = """
|
||||||
|
var el = arguments[0];
|
||||||
|
if (el.__vue__) {
|
||||||
|
// 尝试修改自身绑定的值
|
||||||
|
el.__vue__.value = '2026-05-01 00:00:00';
|
||||||
|
el.__vue__.$emit('input', '2026-05-01 00:00:00');
|
||||||
|
el.__vue__.$emit('change', '2026-05-01 00:00:00');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果外层有包裹的 el-date-picker 父组件
|
||||||
|
var parent = el.closest('.el-date-editor');
|
||||||
|
if (parent && parent.__vue__) {
|
||||||
|
parent.__vue__.value = '2026-05-01 00:00:00';
|
||||||
|
if (parent.__vue__.userInput) {
|
||||||
|
parent.__vue__.userInput = '2026-05-01 00:00:00';
|
||||||
|
}
|
||||||
|
parent.__vue__.$emit('input', '2026-05-01 00:00:00');
|
||||||
|
parent.__vue__.$emit('change', '2026-05-01 00:00:00');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 物理备用手段,防止 Vue 版本差异
|
||||||
|
el.value = '2026-05-01 00:00:00';
|
||||||
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
page.run_js(vue_hack_js, date_input)
|
||||||
|
log("OK", "✅ Vue 注入指令已发送!请肉眼观察输入框是否有变化。")
|
||||||
|
except Exception as e:
|
||||||
|
log("WARN", f"Vue 注入执行时发生警告: {e}")
|
||||||
|
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
log("INFO", "准备点击【查询】按钮触发网络请求...")
|
||||||
|
query_btn_xpath = 'xpath://*[@id="customTable-search-area"]/div[1]/div/div[1]/a[2]/span/span'
|
||||||
|
query_btn = page.ele(query_btn_xpath, timeout=3)
|
||||||
|
|
||||||
|
if query_btn:
|
||||||
|
try: query_btn.click()
|
||||||
|
except: page.run_js("arguments[0].click();", query_btn)
|
||||||
|
log("OK", "✅ 已点击【查询】按钮!请在浏览器中观察页面是否开始刷新 2026-05-01 之后的数据。")
|
||||||
|
else:
|
||||||
|
log("ERR", "找不到查询按钮。")
|
||||||
|
|
||||||
|
else:
|
||||||
|
log("ERR", "找不到日期输入框,请检查 XPath 是否正确!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log("ERR", f"发生异常: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_vue_injection()
|
||||||
210
web_ui/app.py
210
web_ui/app.py
@@ -44,13 +44,31 @@ BROWSER_LOGIN_DIR = BASE_DIR / "browser_login"
|
|||||||
browser_lock = threading.Lock()
|
browser_lock = threading.Lock()
|
||||||
is_browser_busy = False
|
is_browser_busy = False
|
||||||
current_task_name = ""
|
current_task_name = ""
|
||||||
|
task_logs = []
|
||||||
|
|
||||||
|
class WebLogger:
|
||||||
|
def __init__(self, orig):
|
||||||
|
self.orig = orig
|
||||||
|
def write(self, text):
|
||||||
|
self.orig.write(text)
|
||||||
|
msg = text.strip()
|
||||||
|
if msg and is_browser_busy:
|
||||||
|
task_logs.append(msg)
|
||||||
|
if len(task_logs) > 500:
|
||||||
|
task_logs.pop(0)
|
||||||
|
def flush(self):
|
||||||
|
self.orig.flush()
|
||||||
|
|
||||||
|
sys.stdout = WebLogger(sys.stdout)
|
||||||
|
sys.stderr = WebLogger(sys.stderr)
|
||||||
|
|
||||||
def set_browser_busy(task_name):
|
def set_browser_busy(task_name):
|
||||||
"""设置浏览器为忙碌状态"""
|
"""设置浏览器为忙碌状态"""
|
||||||
global is_browser_busy, current_task_name
|
global is_browser_busy, current_task_name, task_logs
|
||||||
with browser_lock:
|
with browser_lock:
|
||||||
is_browser_busy = True
|
is_browser_busy = True
|
||||||
current_task_name = task_name
|
current_task_name = task_name
|
||||||
|
task_logs.clear()
|
||||||
|
|
||||||
def release_browser():
|
def release_browser():
|
||||||
"""释放浏览器控制权"""
|
"""释放浏览器控制权"""
|
||||||
@@ -127,6 +145,16 @@ def receipts_page():
|
|||||||
"""渲染收货明细数据看板"""
|
"""渲染收货明细数据看板"""
|
||||||
return render_template('index.html')
|
return render_template('index.html')
|
||||||
|
|
||||||
|
@app.route('/work_orders')
|
||||||
|
def work_orders_page():
|
||||||
|
"""渲染生产工单数据看板"""
|
||||||
|
return render_template('work_orders.html')
|
||||||
|
|
||||||
|
@app.route('/abnormal_report')
|
||||||
|
def abnormal_report_page():
|
||||||
|
"""渲染发料异常检查数据看板"""
|
||||||
|
return render_template('abnormal_report.html')
|
||||||
|
|
||||||
@app.route('/api/receipts')
|
@app.route('/api/receipts')
|
||||||
def get_receipts():
|
def get_receipts():
|
||||||
"""获取收货明细数据(支持分页和多条件搜索)"""
|
"""获取收货明细数据(支持分页和多条件搜索)"""
|
||||||
@@ -175,6 +203,102 @@ def get_receipts():
|
|||||||
"rows": [dict(ix) for ix in receipts]
|
"rows": [dict(ix) for ix in receipts]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@app.route('/api/work_orders')
|
||||||
|
def get_work_orders():
|
||||||
|
"""获取生产工单数据(支持分页和多条件搜索)"""
|
||||||
|
page = int(request.args.get('page', 1))
|
||||||
|
limit = int(request.args.get('limit', 50))
|
||||||
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
|
# 获取搜索参数
|
||||||
|
wo_number = request.args.get('work_orders_number', '').strip()
|
||||||
|
material_name = request.args.get('material_name', '').strip()
|
||||||
|
material_code = request.args.get('material_code', '').strip()
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
|
||||||
|
# 构建动态 SQL 查询
|
||||||
|
query_conditions = []
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if wo_number:
|
||||||
|
query_conditions.append("work_orders_number LIKE ?")
|
||||||
|
params.append(f"%{wo_number}%")
|
||||||
|
if material_name:
|
||||||
|
query_conditions.append("material_name LIKE ?")
|
||||||
|
params.append(f"%{material_name}%")
|
||||||
|
if material_code:
|
||||||
|
query_conditions.append("material_code LIKE ?")
|
||||||
|
params.append(f"%{material_code}%")
|
||||||
|
|
||||||
|
where_clause = ""
|
||||||
|
if query_conditions:
|
||||||
|
where_clause = " WHERE " + " AND ".join(query_conditions)
|
||||||
|
|
||||||
|
# 获取总数
|
||||||
|
count_query = f"SELECT COUNT(*) FROM work_orders{where_clause}"
|
||||||
|
total = conn.execute(count_query, params).fetchone()[0]
|
||||||
|
|
||||||
|
# 获取分页数据
|
||||||
|
data_query = f"SELECT * FROM work_orders{where_clause} ORDER BY execution_time DESC LIMIT ? OFFSET ?"
|
||||||
|
params.extend([limit, offset])
|
||||||
|
orders = conn.execute(data_query, params).fetchall()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"total": total,
|
||||||
|
"rows": [dict(ix) for ix in orders]
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/api/abnormal_report')
|
||||||
|
def get_abnormal_report():
|
||||||
|
"""获取发料异常报表数据(支持分页和多条件搜索)"""
|
||||||
|
page = int(request.args.get('page', 1))
|
||||||
|
limit = int(request.args.get('limit', 50))
|
||||||
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
|
# 获取搜索参数
|
||||||
|
wo_number = request.args.get('work_orders_number', '').strip()
|
||||||
|
material_code = request.args.get('material_code', '').strip()
|
||||||
|
issue_status = request.args.get('issue_status', '').strip()
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
|
||||||
|
# 构建动态 SQL 查询
|
||||||
|
query_conditions = []
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if wo_number:
|
||||||
|
query_conditions.append("work_orders_number LIKE ?")
|
||||||
|
params.append(f"%{wo_number}%")
|
||||||
|
if material_code:
|
||||||
|
query_conditions.append("material_code LIKE ?")
|
||||||
|
params.append(f"%{material_code}%")
|
||||||
|
if issue_status:
|
||||||
|
query_conditions.append("issue_status = ?")
|
||||||
|
params.append(issue_status)
|
||||||
|
|
||||||
|
where_clause = ""
|
||||||
|
if query_conditions:
|
||||||
|
where_clause = " WHERE " + " AND ".join(query_conditions)
|
||||||
|
|
||||||
|
# 获取总数
|
||||||
|
count_query = f"SELECT COUNT(*) FROM abnormal_report{where_clause}"
|
||||||
|
total = conn.execute(count_query, params).fetchone()[0]
|
||||||
|
|
||||||
|
# 获取分页数据 (默认按工单号倒序排列)
|
||||||
|
data_query = f"SELECT * FROM abnormal_report{where_clause} ORDER BY work_orders_number DESC LIMIT ? OFFSET ?"
|
||||||
|
params.extend([limit, offset])
|
||||||
|
orders = conn.execute(data_query, params).fetchall()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"total": total,
|
||||||
|
"rows": [dict(ix) for ix in orders]
|
||||||
|
})
|
||||||
|
|
||||||
@app.route('/api/task_status')
|
@app.route('/api/task_status')
|
||||||
def get_task_status():
|
def get_task_status():
|
||||||
"""获取当前浏览器控制任务的状态"""
|
"""获取当前浏览器控制任务的状态"""
|
||||||
@@ -183,6 +307,11 @@ def get_task_status():
|
|||||||
"task_name": current_task_name
|
"task_name": current_task_name
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@app.route('/api/task_logs')
|
||||||
|
def get_task_logs():
|
||||||
|
"""获取实时日志"""
|
||||||
|
return jsonify({"logs": task_logs})
|
||||||
|
|
||||||
@app.route('/api/sync_receipts', methods=['POST'])
|
@app.route('/api/sync_receipts', methods=['POST'])
|
||||||
def sync_receipts():
|
def sync_receipts():
|
||||||
"""触发后台运行收货明细增量抓取脚本(修复跨机器路径问题)"""
|
"""触发后台运行收货明细增量抓取脚本(修复跨机器路径问题)"""
|
||||||
@@ -222,6 +351,85 @@ def sync_receipts():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"success": False, "message": f"系统错误: {str(e)}"}), 500
|
return jsonify({"success": False, "message": f"系统错误: {str(e)}"}), 500
|
||||||
|
|
||||||
|
@app.route('/api/sync_work_orders', methods=['POST'])
|
||||||
|
def sync_work_orders():
|
||||||
|
"""触发后台运行生产工单增量抓取脚本"""
|
||||||
|
global is_browser_busy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if is_browser_busy:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": f"系统忙碌:当前正在执行 '{current_task_name}',请稍后再试。"
|
||||||
|
}), 409
|
||||||
|
|
||||||
|
if str(BROWSER_LOGIN_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(BROWSER_LOGIN_DIR))
|
||||||
|
|
||||||
|
def run_work_orders_sync():
|
||||||
|
set_browser_busy("手动生产工单增量同步")
|
||||||
|
try:
|
||||||
|
from fetch_work_orders_incremental import fetch_work_orders_incremental
|
||||||
|
fetch_work_orders_incremental()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"手动生产工单同步失败: {e}")
|
||||||
|
finally:
|
||||||
|
release_browser()
|
||||||
|
|
||||||
|
try:
|
||||||
|
threading.Thread(target=run_work_orders_sync, daemon=True).start()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": "工单增量同步任务已在后台启动!请观察黑框控制台的运行日志。",
|
||||||
|
"logs": "任务已在后台运行..."
|
||||||
|
})
|
||||||
|
except ImportError:
|
||||||
|
return jsonify({"success": False, "message": "找不到增量抓取脚本或导入失败"}), 404
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": False, "message": f"系统错误: {str(e)}"}), 500
|
||||||
|
|
||||||
|
@app.route('/api/sync_abnormal_report', methods=['POST'])
|
||||||
|
def sync_abnormal_report():
|
||||||
|
"""触发后台运行异常报表抓取脚本"""
|
||||||
|
global is_browser_busy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if is_browser_busy:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": f"系统忙碌:当前正在执行 '{current_task_name}',请稍后再试。"
|
||||||
|
}), 409
|
||||||
|
|
||||||
|
if str(BROWSER_LOGIN_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(BROWSER_LOGIN_DIR))
|
||||||
|
|
||||||
|
def run_abnormal_report_sync():
|
||||||
|
set_browser_busy("手动发料异常报表抓取")
|
||||||
|
try:
|
||||||
|
import auto_fetch_abnormal_report
|
||||||
|
page = auto_fetch_abnormal_report.get_page(port=9222)
|
||||||
|
success = auto_fetch_abnormal_report.navigate_to_report(page)
|
||||||
|
if success:
|
||||||
|
auto_fetch_abnormal_report.fetch_report_data(page)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"手动发料异常报表抓取失败: {e}")
|
||||||
|
finally:
|
||||||
|
release_browser()
|
||||||
|
|
||||||
|
try:
|
||||||
|
threading.Thread(target=run_abnormal_report_sync, daemon=True).start()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": "发料异常报表抓取任务已在后台启动!请观察黑框控制台的运行日志。",
|
||||||
|
"logs": "任务已在后台运行..."
|
||||||
|
})
|
||||||
|
except ImportError:
|
||||||
|
return jsonify({"success": False, "message": "找不到异常报表抓取脚本或导入失败"}), 404
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": False, "message": f"系统错误: {str(e)}"}), 500
|
||||||
|
|
||||||
@app.route('/api/sync_bom', methods=['POST'])
|
@app.route('/api/sync_bom', methods=['POST'])
|
||||||
def sync_bom():
|
def sync_bom():
|
||||||
"""触发后台运行 BOM 成本全量抓取脚本(修复跨机器路径问题)"""
|
"""触发后台运行 BOM 成本全量抓取脚本(修复跨机器路径问题)"""
|
||||||
|
|||||||
287
web_ui/templates/abnormal_report.html
Normal file
287
web_ui/templates/abnormal_report.html
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>发料异常检查报表</title>
|
||||||
|
<!-- 引入 ElementUI 样式 -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
|
||||||
|
<!-- 引入 Vue.js -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
|
||||||
|
<!-- 引入 ElementUI 组件库 -->
|
||||||
|
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
|
||||||
|
<!-- 引入 axios -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 20px; font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif; background-color: #f0f2f5; }
|
||||||
|
.box-card { margin-bottom: 20px; }
|
||||||
|
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||||
|
.page-header h2 { margin: 0; color: #303133; }
|
||||||
|
.search-row { display: flex; gap: 15px; margin-bottom: 20px; }
|
||||||
|
.pagination-container { margin-top: 20px; text-align: right; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<el-card class="box-card">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2><i class="el-icon-warning-outline" style="margin-right: 10px; color: #F56C6C;"></i>发料异常检查报表</h2>
|
||||||
|
<div>
|
||||||
|
<el-button type="info" plain icon="el-icon-back" @click="goBack" size="small">返回主控台</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索区域 -->
|
||||||
|
<div class="search-row">
|
||||||
|
<el-input v-model="searchParams.work_orders_number" placeholder="工单号 (支持模糊搜索)" style="width: 250px" clearable @clear="handleSearch" @keyup.enter.native="handleSearch">
|
||||||
|
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||||
|
</el-input>
|
||||||
|
<el-input v-model="searchParams.material_code" placeholder="物料代码 (支持模糊搜索)" style="width: 250px" clearable @clear="handleSearch" @keyup.enter.native="handleSearch">
|
||||||
|
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||||
|
</el-input>
|
||||||
|
<el-select v-model="searchParams.issue_status" placeholder="发料情况" style="width: 150px" clearable @change="handleSearch">
|
||||||
|
<el-option label="全部" value=""></el-option>
|
||||||
|
<el-option label="发料正常" value="发料正常"></el-option>
|
||||||
|
<el-option label="未发料" value="未发料"></el-option>
|
||||||
|
</el-select>
|
||||||
|
<el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh-left" @click="resetSearch">重置</el-button>
|
||||||
|
<el-button type="warning" :icon="isSystemBusy ? 'el-icon-loading' : 'el-icon-download'" :disabled="isSystemBusy" @click="syncAbnormalReport">
|
||||||
|
<span v-text="isSystemBusy ? '抓取中...' : '抓取异常报表'"></span>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 全局忙碌提示条 -->
|
||||||
|
<el-alert
|
||||||
|
v-if="isSystemBusy && globalTaskName"
|
||||||
|
type="warning"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom: 15px;">
|
||||||
|
<template slot="title">
|
||||||
|
<div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">
|
||||||
|
<span>系统忙碌中:正在执行 {{ globalTaskName }},请耐心等待完成后再操作。</span>
|
||||||
|
<el-button size="mini" type="primary" plain @click="showLogDialog = true" style="margin-left: 15px;">查看执行进度</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<!-- 日志弹窗 -->
|
||||||
|
<el-dialog title="后台执行日志 (实时)" :visible.sync="showLogDialog" width="60%">
|
||||||
|
<div id="log-container" style="background-color: #1e1e1e; color: #67C23A; padding: 15px; height: 350px; overflow-y: auto; font-family: Consolas, monospace; border-radius: 4px; line-height: 1.5; font-size: 14px;">
|
||||||
|
<div v-for="(log, index) in taskLogs" :key="index" v-text="log"></div>
|
||||||
|
<div v-if="taskLogs.length === 0" style="color: #909399;">正在启动任务,等待输出...</div>
|
||||||
|
</div>
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="showLogDialog = false">关闭窗口 (后台会继续执行)</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<el-table :data="tableData" v-loading="loading" border style="width: 100%" stripe size="small" :header-cell-style="{background:'#f5f7fa',color:'#606266'}">
|
||||||
|
<el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
|
||||||
|
<el-table-column prop="work_orders_number" label="生产工单号" width="160"></el-table-column>
|
||||||
|
<el-table-column prop="product_code" label="产品代码" width="120"></el-table-column>
|
||||||
|
<el-table-column prop="product_name" label="产品名称" min-width="150" show-overflow-tooltip></el-table-column>
|
||||||
|
<el-table-column prop="status" label="工单状态" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.status === '作业中' ? 'primary' : 'success'" size="small" v-text="scope.row.status"></el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="order_date" label="下单日期" width="120"></el-table-column>
|
||||||
|
<el-table-column prop="workshop" label="生产车间" width="120"></el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="需求物料信息" align="center">
|
||||||
|
<el-table-column prop="material_code" label="需求物料代码" width="120"></el-table-column>
|
||||||
|
<el-table-column prop="material_name" label="需求物料名称" min-width="150" show-overflow-tooltip></el-table-column>
|
||||||
|
<el-table-column prop="material_specification" label="规格" min-width="120" show-overflow-tooltip></el-table-column>
|
||||||
|
<el-table-column prop="unit_qty" label="单机用量" width="80" align="right"></el-table-column>
|
||||||
|
<el-table-column prop="total_demand_qty" label="需求总量" width="90" align="right"></el-table-column>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="发料异常比对" align="center">
|
||||||
|
<el-table-column prop="warehouse_issue_qty" label="仓库发放数量" width="100" align="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span style="font-weight: bold;" v-text="scope.row.warehouse_issue_qty"></span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="theoretical_issue_qty" label="理论出料数量" width="100" align="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span style="color: #909399;" v-text="scope.row.theoretical_issue_qty"></span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="差异" width="90" align="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span :style="{
|
||||||
|
color: (scope.row.warehouse_issue_qty - scope.row.theoretical_issue_qty) > 0 ? '#F56C6C' : '#67C23A',
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}" v-text="(scope.row.warehouse_issue_qty - scope.row.theoretical_issue_qty).toFixed(2)">
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="issue_status" label="发料情况" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.issue_status === '超领发料' ? 'danger' : (scope.row.issue_status === '未发料' ? 'info' : 'warning')" size="small" v-text="scope.row.issue_status">
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
:current-page="currentPage"
|
||||||
|
:page-sizes="[20, 50, 100, 200]"
|
||||||
|
:page-size="pageSize"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="total">
|
||||||
|
</el-pagination>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
new Vue({
|
||||||
|
el: '#app',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
isSystemBusy: false,
|
||||||
|
globalTaskName: "",
|
||||||
|
statusTimer: null,
|
||||||
|
showLogDialog: false,
|
||||||
|
taskLogs: [],
|
||||||
|
logTimer: null,
|
||||||
|
tableData: [],
|
||||||
|
total: 0,
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 50,
|
||||||
|
searchParams: {
|
||||||
|
work_orders_number: '',
|
||||||
|
material_code: '',
|
||||||
|
issue_status: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchData();
|
||||||
|
this.checkTaskStatus();
|
||||||
|
this.statusTimer = setInterval(this.checkTaskStatus, 3000);
|
||||||
|
this.logTimer = setInterval(this.fetchLogs, 1000);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.statusTimer) {
|
||||||
|
clearInterval(this.statusTimer);
|
||||||
|
}
|
||||||
|
if (this.logTimer) {
|
||||||
|
clearInterval(this.logTimer);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchLogs() {
|
||||||
|
if (this.isSystemBusy && this.showLogDialog) {
|
||||||
|
axios.get('/api/task_logs')
|
||||||
|
.then(res => {
|
||||||
|
this.taskLogs = res.data.logs;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const container = document.getElementById('log-container');
|
||||||
|
if (container) {
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checkTaskStatus() {
|
||||||
|
axios.get('/api/task_status')
|
||||||
|
.then(res => {
|
||||||
|
this.isSystemBusy = res.data.is_busy;
|
||||||
|
this.globalTaskName = res.data.task_name;
|
||||||
|
})
|
||||||
|
.catch(err => {});
|
||||||
|
},
|
||||||
|
goBack() {
|
||||||
|
window.location.href = '/';
|
||||||
|
},
|
||||||
|
fetchData() {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
page: this.currentPage,
|
||||||
|
limit: this.pageSize,
|
||||||
|
work_orders_number: this.searchParams.work_orders_number,
|
||||||
|
material_code: this.searchParams.material_code,
|
||||||
|
issue_status: this.searchParams.issue_status
|
||||||
|
};
|
||||||
|
|
||||||
|
axios.get('/api/abnormal_report', { params })
|
||||||
|
.then(res => {
|
||||||
|
this.tableData = res.data.rows;
|
||||||
|
this.total = res.data.total;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.$message.error('获取数据失败');
|
||||||
|
console.error(err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.fetchData();
|
||||||
|
},
|
||||||
|
resetSearch() {
|
||||||
|
this.searchParams.work_orders_number = '';
|
||||||
|
this.searchParams.material_code = '';
|
||||||
|
this.searchParams.issue_status = '';
|
||||||
|
this.handleSearch();
|
||||||
|
},
|
||||||
|
handleSizeChange(val) {
|
||||||
|
this.pageSize = val;
|
||||||
|
this.fetchData();
|
||||||
|
},
|
||||||
|
handleCurrentChange(val) {
|
||||||
|
this.currentPage = val;
|
||||||
|
this.fetchData();
|
||||||
|
},
|
||||||
|
syncAbnormalReport() {
|
||||||
|
this.$confirm('确定要抓取发料异常报表吗?该操作会在后台打开 ERP 进行翻页抓取。', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
this.isSystemBusy = true; // 立即将状态置为 busy,展示 loading
|
||||||
|
axios.post('/api/sync_abnormal_report')
|
||||||
|
.then(res => {
|
||||||
|
if (res.data.success) {
|
||||||
|
this.$message.success(res.data.message);
|
||||||
|
setTimeout(this.checkTaskStatus, 500);
|
||||||
|
// 自动弹开日志窗口让用户看进度
|
||||||
|
this.showLogDialog = true;
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.data.message);
|
||||||
|
this.isSystemBusy = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.response && err.response.status === 409) {
|
||||||
|
this.$message.warning(err.response.data.message);
|
||||||
|
} else {
|
||||||
|
this.$message.error(err.response?.data?.message || '触发抓取异常报表失败');
|
||||||
|
}
|
||||||
|
this.isSystemBusy = false;
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -104,6 +104,12 @@
|
|||||||
.card-bom-compare:hover { border-color: #E6A23C; background-color: #fdf6ec; }
|
.card-bom-compare:hover { border-color: #E6A23C; background-color: #fdf6ec; }
|
||||||
.card-bom-compare i { color: #E6A23C; }
|
.card-bom-compare i { color: #E6A23C; }
|
||||||
|
|
||||||
|
.card-work-order { border-top: 4px solid #E6A23C; }
|
||||||
|
.card-work-order i { color: #E6A23C; }
|
||||||
|
|
||||||
|
.card-abnormal { border-top: 4px solid #F56C6C; }
|
||||||
|
.card-abnormal i { color: #F56C6C; }
|
||||||
|
|
||||||
.action-group {
|
.action-group {
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
padding-top: 30px;
|
padding-top: 30px;
|
||||||
@@ -142,6 +148,20 @@
|
|||||||
<h3>期间成本对比分析表</h3>
|
<h3>期间成本对比分析表</h3>
|
||||||
<p>跨时间段核算 BOM 最新价差异,支持虚拟件过滤与历史价回溯。</p>
|
<p>跨时间段核算 BOM 最新价差异,支持虚拟件过滤与历史价回溯。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡片 4: 生产工单明细 -->
|
||||||
|
<div class="nav-card card-work-order" onclick="window.location.href='/work_orders'">
|
||||||
|
<i class="el-icon-document"></i>
|
||||||
|
<h3>生产工单明细</h3>
|
||||||
|
<p>查询生产工单记录、领料情况及执行状态。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡片 5: 发料异常检查 -->
|
||||||
|
<div class="nav-card card-abnormal" onclick="window.location.href='/abnormal_report'">
|
||||||
|
<i class="el-icon-warning-outline"></i>
|
||||||
|
<h3>发料异常检查</h3>
|
||||||
|
<p>排查生产工单的发料异常,对比理论出料与实际发放数量的差异。</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="action-group">
|
<div class="action-group">
|
||||||
@@ -166,6 +186,15 @@
|
|||||||
round>
|
round>
|
||||||
<span v-text="syncingBom ? '请求已发送...' : '读取最新 BOM 表'"></span>
|
<span v-text="syncingBom ? '请求已发送...' : '读取最新 BOM 表'"></span>
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
:icon="isSystemBusy ? 'el-icon-loading' : 'el-icon-refresh'"
|
||||||
|
:disabled="isSystemBusy"
|
||||||
|
@click="syncWorkOrders"
|
||||||
|
round>
|
||||||
|
<span v-text="syncingWorkOrders ? '请求已发送...' : '读取生产工单明细'"></span>
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -176,6 +205,7 @@
|
|||||||
return {
|
return {
|
||||||
syncing: false,
|
syncing: false,
|
||||||
syncingBom: false,
|
syncingBom: false,
|
||||||
|
syncingWorkOrders: false,
|
||||||
isSystemBusy: false,
|
isSystemBusy: false,
|
||||||
globalTaskName: "",
|
globalTaskName: "",
|
||||||
statusTimer: null
|
statusTimer: null
|
||||||
@@ -249,6 +279,29 @@
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.syncingBom = false;
|
this.syncingBom = false;
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
syncWorkOrders() {
|
||||||
|
this.syncingWorkOrders = true;
|
||||||
|
|
||||||
|
axios.post('/api/sync_work_orders')
|
||||||
|
.then(res => {
|
||||||
|
if (res.data.success) {
|
||||||
|
this.$message.success('已触发!' + res.data.message);
|
||||||
|
setTimeout(this.checkTaskStatus, 500);
|
||||||
|
} else {
|
||||||
|
this.$message.error('触发失败:' + res.data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.response && err.response.status === 409) {
|
||||||
|
this.$message.warning(err.response.data.message);
|
||||||
|
} else {
|
||||||
|
this.$message.error('请求发生异常,请检查后端日志。');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.syncingWorkOrders = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
171
web_ui/templates/work_orders.html
Normal file
171
web_ui/templates/work_orders.html
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>生产工单明细看板</title>
|
||||||
|
<!-- 引入 ElementUI 样式 -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
|
||||||
|
<!-- 引入 Vue.js -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
|
||||||
|
<!-- 引入 ElementUI 组件库 -->
|
||||||
|
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
|
||||||
|
<!-- 引入 axios -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 20px; font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif; background-color: #f0f2f5; }
|
||||||
|
.box-card { margin-bottom: 20px; }
|
||||||
|
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||||
|
.page-header h2 { margin: 0; color: #303133; }
|
||||||
|
.search-row { display: flex; gap: 15px; margin-bottom: 20px; }
|
||||||
|
.pagination-container { margin-top: 20px; text-align: right; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<el-card class="box-card">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2><i class="el-icon-document" style="margin-right: 10px; color: #E6A23C;"></i>生产工单明细</h2>
|
||||||
|
<div>
|
||||||
|
<el-button type="info" plain icon="el-icon-back" @click="goBack" size="small">返回主控台</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索区域 -->
|
||||||
|
<div class="search-row">
|
||||||
|
<el-input v-model="searchParams.work_orders_number" placeholder="工单号 (支持模糊搜索)" style="width: 250px" clearable @clear="handleSearch" @keyup.enter.native="handleSearch">
|
||||||
|
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||||
|
</el-input>
|
||||||
|
<el-input v-model="searchParams.material_name" placeholder="物料名称 (支持模糊搜索)" style="width: 250px" clearable @clear="handleSearch" @keyup.enter.native="handleSearch">
|
||||||
|
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||||
|
</el-input>
|
||||||
|
<el-input v-model="searchParams.material_code" placeholder="物料代码 (支持模糊搜索)" style="width: 250px" clearable @clear="handleSearch" @keyup.enter.native="handleSearch">
|
||||||
|
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||||
|
</el-input>
|
||||||
|
<el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh-left" @click="resetSearch">重置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<el-table :data="tableData" v-loading="loading" border style="width: 100%" stripe size="small" :header-cell-style="{background:'#f5f7fa',color:'#606266'}">
|
||||||
|
<el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
|
||||||
|
<el-table-column prop="execution_time" label="执行时间" width="160" sortable></el-table-column>
|
||||||
|
<el-table-column prop="work_orders_number" label="工单号" width="160"></el-table-column>
|
||||||
|
<el-table-column prop="line_number" label="行号" width="60" align="center"></el-table-column>
|
||||||
|
<el-table-column prop="material_code" label="物料代码" width="120"></el-table-column>
|
||||||
|
<el-table-column prop="material_name" label="物料名称" min-width="150" show-overflow-tooltip></el-table-column>
|
||||||
|
<el-table-column prop="material_specification" label="规格" min-width="150" show-overflow-tooltip></el-table-column>
|
||||||
|
<el-table-column prop="warehouse_name" label="仓库" width="120" show-overflow-tooltip></el-table-column>
|
||||||
|
<el-table-column label="数量信息" align="center">
|
||||||
|
<el-table-column prop="issue_number" label="应发数量" width="90" align="right"></el-table-column>
|
||||||
|
<el-table-column prop="total_issue_number" label="已发料数量" width="100" align="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span :style="{color: scope.row.total_issue_number >= scope.row.issue_number ? '#67C23A' : '#F56C6C', fontWeight: 'bold'}" v-text="scope.row.total_issue_number">
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="unit_name" label="单位" width="60" align="center"></el-table-column>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="金额信息" align="center">
|
||||||
|
<el-table-column prop="cost_price" label="成本单价" width="100" align="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-text="'¥ ' + Number(scope.row.cost_price || 0).toFixed(2)"></span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="issue_amount_total" label="发料总额" width="100" align="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span style="color: #409EFF; font-weight: bold;" v-text="'¥ ' + Number(scope.row.issue_amount_total || 0).toFixed(2)"></span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="executor_user_name" label="执行人" width="100" align="center"></el-table-column>
|
||||||
|
<el-table-column prop="materials_user_name" label="领料人" width="100" align="center"></el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
:current-page="currentPage"
|
||||||
|
:page-sizes="[20, 50, 100, 200]"
|
||||||
|
:page-size="pageSize"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="total">
|
||||||
|
</el-pagination>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
new Vue({
|
||||||
|
el: '#app',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
syncingReport: false,
|
||||||
|
tableData: [],
|
||||||
|
total: 0,
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 50,
|
||||||
|
searchParams: {
|
||||||
|
work_orders_number: '',
|
||||||
|
material_name: '',
|
||||||
|
material_code: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goBack() {
|
||||||
|
window.location.href = '/';
|
||||||
|
},
|
||||||
|
fetchData() {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
page: this.currentPage,
|
||||||
|
limit: this.pageSize,
|
||||||
|
work_orders_number: this.searchParams.work_orders_number,
|
||||||
|
material_name: this.searchParams.material_name,
|
||||||
|
material_code: this.searchParams.material_code
|
||||||
|
};
|
||||||
|
|
||||||
|
axios.get('/api/work_orders', { params })
|
||||||
|
.then(res => {
|
||||||
|
this.tableData = res.data.rows;
|
||||||
|
this.total = res.data.total;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.$message.error('获取数据失败');
|
||||||
|
console.error(err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.fetchData();
|
||||||
|
},
|
||||||
|
resetSearch() {
|
||||||
|
this.searchParams.work_orders_number = '';
|
||||||
|
this.searchParams.material_name = '';
|
||||||
|
this.searchParams.material_code = '';
|
||||||
|
this.handleSearch();
|
||||||
|
},
|
||||||
|
handleSizeChange(val) {
|
||||||
|
this.pageSize = val;
|
||||||
|
this.fetchData();
|
||||||
|
},
|
||||||
|
handleCurrentChange(val) {
|
||||||
|
this.currentPage = val;
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user