抓取生产工单,赚取发料异常

This commit is contained in:
hjq
2026-06-11 15:58:56 +08:00
parent 66eecd0daa
commit 5b19790037
40 changed files with 4942 additions and 54 deletions

5
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

9
.idea/Datie.iml generated Normal file
View 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
View 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
View 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
View 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>

View 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

View 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)

View 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}")

View 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)} 个输入框")

View 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)

View 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()

View 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)

View 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("没有找到 查询 按钮")

View 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("没有找到 查询 按钮")

View 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()

View 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("没找到 进入自定义报表 按钮")

View 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("没有找到查询按钮")

View 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()

View 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()

View 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()

View 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()

View 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)

View 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()

View 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)

View 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)

View 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.")

View File

@@ -6,6 +6,7 @@ from config import OUTPUT_DIR, DB_PATH
RECEIPT_JSON = OUTPUT_DIR / "receipt_details_full_clean.json"
BOM_JSON = OUTPUT_DIR / "bom_cost_full_tree_final.json"
ISSUE_JSON = OUTPUT_DIR / "issue_receipt_details_full.json"
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_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
cursor.execute('''
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_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()
return conn
@@ -80,68 +218,74 @@ def import_receipt_details(conn):
data = json.load(f)
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:
p_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 (
purchase_order_code, row_no, material_code, material_name,
material_specification, warehouse_code, warehouse_name,
supplier_code, supplier_name, unit_name, conversion_unit,
receive_price, receipt_time,
purchase_qty, receive_qty, total_amount
) 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("收货时间"),
p_qty,
r_qty,
item.get("收货总金额")
))
count += 1
# 容错:如果关键字段为空则跳过
if not po_code or not row_no or not mat_code:
continue
try:
cursor.execute('''
INSERT INTO receipt_details (
purchase_order_code, row_no, material_code, material_name,
material_specification, warehouse_code, warehouse_name,
supplier_code, supplier_name, unit_name, conversion_unit,
receive_price, receipt_time,
purchase_qty, receive_qty, total_amount
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(purchase_order_code, row_no, material_code) DO UPDATE SET
receive_price=excluded.receive_price,
receive_qty=excluded.receive_qty,
total_amount=excluded.total_amount,
receipt_time=excluded.receipt_time
''', (
po_code,
row_no,
mat_code,
item.get("物料名称"),
item.get("物料规格"),
item.get("仓库代码"),
item.get("仓库名称"),
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()
print(f"成功导入 {count} 条收货明细数据!")
print(f"成功处理(新增或更新) {count_inserted} 条收货明细数据!")
def _insert_bom_tree(cursor, parent_material_code, tree_nodes, parent_node_id=None):
"""递归插入 BOM 树节点"""
@@ -213,6 +357,128 @@ def import_bom_data(conn):
child_count = cursor.fetchone()[0]
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 时,如果发生了 UPDATErowcount 也是 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__":
import sys
print(f"数据库文件将保存在: {DB_PATH}")
@@ -225,10 +491,13 @@ if __name__ == "__main__":
import_bom_data(conn)
elif "--receipt-only" in args:
import_receipt_details(conn)
elif "--issue-only" in args:
import_issue_receipt_details(conn)
else:
# 默认全量导入
import_receipt_details(conn)
import_bom_data(conn)
import_issue_receipt_details(conn)
conn.close()
print("全部导入完成!你可以使用 SQLite 客户端连接 erp_data.db 查看数据。")

View 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)

View 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)

View 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)

View 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

View 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'))

View 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)

View 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()

View 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()

1637
page.html Normal file

File diff suppressed because one or more lines are too long

View File

@@ -44,13 +44,31 @@ BROWSER_LOGIN_DIR = BASE_DIR / "browser_login"
browser_lock = threading.Lock()
is_browser_busy = False
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):
"""设置浏览器为忙碌状态"""
global is_browser_busy, current_task_name
global is_browser_busy, current_task_name, task_logs
with browser_lock:
is_browser_busy = True
current_task_name = task_name
task_logs.clear()
def release_browser():
"""释放浏览器控制权"""
@@ -127,6 +145,16 @@ def receipts_page():
"""渲染收货明细数据看板"""
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')
def get_receipts():
"""获取收货明细数据(支持分页和多条件搜索)"""
@@ -175,6 +203,102 @@ def get_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')
def get_task_status():
"""获取当前浏览器控制任务的状态"""
@@ -183,6 +307,11 @@ def get_task_status():
"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'])
def sync_receipts():
"""触发后台运行收货明细增量抓取脚本(修复跨机器路径问题)"""
@@ -222,6 +351,85 @@ def sync_receipts():
except Exception as e:
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'])
def sync_bom():
"""触发后台运行 BOM 成本全量抓取脚本(修复跨机器路径问题)"""

View 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>

View File

@@ -104,6 +104,12 @@
.card-bom-compare:hover { border-color: #E6A23C; background-color: #fdf6ec; }
.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 {
margin-top: 40px;
padding-top: 30px;
@@ -142,6 +148,20 @@
<h3>期间成本对比分析表</h3>
<p>跨时间段核算 BOM 最新价差异,支持虚拟件过滤与历史价回溯。</p>
</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 class="action-group">
@@ -166,6 +186,15 @@
round>
<span v-text="syncingBom ? '请求已发送...' : '读取最新 BOM 表'"></span>
</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>
@@ -176,6 +205,7 @@
return {
syncing: false,
syncingBom: false,
syncingWorkOrders: false,
isSystemBusy: false,
globalTaskName: "",
statusTimer: null
@@ -249,6 +279,29 @@
.finally(() => {
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;
});
}
}
});

View 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>