抓取生产工单,赚取发料异常
This commit is contained in:
210
web_ui/app.py
210
web_ui/app.py
@@ -44,13 +44,31 @@ BROWSER_LOGIN_DIR = BASE_DIR / "browser_login"
|
||||
browser_lock = threading.Lock()
|
||||
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 成本全量抓取脚本(修复跨机器路径问题)"""
|
||||
|
||||
287
web_ui/templates/abnormal_report.html
Normal file
287
web_ui/templates/abnormal_report.html
Normal file
@@ -0,0 +1,287 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>发料异常检查报表</title>
|
||||
<!-- 引入 ElementUI 样式 -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
|
||||
<!-- 引入 Vue.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
|
||||
<!-- 引入 ElementUI 组件库 -->
|
||||
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
|
||||
<!-- 引入 axios -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<style>
|
||||
body { margin: 0; padding: 20px; font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif; background-color: #f0f2f5; }
|
||||
.box-card { margin-bottom: 20px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.page-header h2 { margin: 0; color: #303133; }
|
||||
.search-row { display: flex; gap: 15px; margin-bottom: 20px; }
|
||||
.pagination-container { margin-top: 20px; text-align: right; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<el-card class="box-card">
|
||||
<div class="page-header">
|
||||
<h2><i class="el-icon-warning-outline" style="margin-right: 10px; color: #F56C6C;"></i>发料异常检查报表</h2>
|
||||
<div>
|
||||
<el-button type="info" plain icon="el-icon-back" @click="goBack" size="small">返回主控台</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-row">
|
||||
<el-input v-model="searchParams.work_orders_number" placeholder="工单号 (支持模糊搜索)" style="width: 250px" clearable @clear="handleSearch" @keyup.enter.native="handleSearch">
|
||||
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||
</el-input>
|
||||
<el-input v-model="searchParams.material_code" placeholder="物料代码 (支持模糊搜索)" style="width: 250px" clearable @clear="handleSearch" @keyup.enter.native="handleSearch">
|
||||
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||
</el-input>
|
||||
<el-select v-model="searchParams.issue_status" placeholder="发料情况" style="width: 150px" clearable @change="handleSearch">
|
||||
<el-option label="全部" value=""></el-option>
|
||||
<el-option label="发料正常" value="发料正常"></el-option>
|
||||
<el-option label="未发料" value="未发料"></el-option>
|
||||
</el-select>
|
||||
<el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh-left" @click="resetSearch">重置</el-button>
|
||||
<el-button type="warning" :icon="isSystemBusy ? 'el-icon-loading' : 'el-icon-download'" :disabled="isSystemBusy" @click="syncAbnormalReport">
|
||||
<span v-text="isSystemBusy ? '抓取中...' : '抓取异常报表'"></span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 全局忙碌提示条 -->
|
||||
<el-alert
|
||||
v-if="isSystemBusy && globalTaskName"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-bottom: 15px;">
|
||||
<template slot="title">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">
|
||||
<span>系统忙碌中:正在执行 {{ globalTaskName }},请耐心等待完成后再操作。</span>
|
||||
<el-button size="mini" type="primary" plain @click="showLogDialog = true" style="margin-left: 15px;">查看执行进度</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<!-- 日志弹窗 -->
|
||||
<el-dialog title="后台执行日志 (实时)" :visible.sync="showLogDialog" width="60%">
|
||||
<div id="log-container" style="background-color: #1e1e1e; color: #67C23A; padding: 15px; height: 350px; overflow-y: auto; font-family: Consolas, monospace; border-radius: 4px; line-height: 1.5; font-size: 14px;">
|
||||
<div v-for="(log, index) in taskLogs" :key="index" v-text="log"></div>
|
||||
<div v-if="taskLogs.length === 0" style="color: #909399;">正在启动任务,等待输出...</div>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="showLogDialog = false">关闭窗口 (后台会继续执行)</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="tableData" v-loading="loading" border style="width: 100%" stripe size="small" :header-cell-style="{background:'#f5f7fa',color:'#606266'}">
|
||||
<el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
|
||||
<el-table-column prop="work_orders_number" label="生产工单号" width="160"></el-table-column>
|
||||
<el-table-column prop="product_code" label="产品代码" width="120"></el-table-column>
|
||||
<el-table-column prop="product_name" label="产品名称" min-width="150" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="status" label="工单状态" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.status === '作业中' ? 'primary' : 'success'" size="small" v-text="scope.row.status"></el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="order_date" label="下单日期" width="120"></el-table-column>
|
||||
<el-table-column prop="workshop" label="生产车间" width="120"></el-table-column>
|
||||
|
||||
<el-table-column label="需求物料信息" align="center">
|
||||
<el-table-column prop="material_code" label="需求物料代码" width="120"></el-table-column>
|
||||
<el-table-column prop="material_name" label="需求物料名称" min-width="150" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="material_specification" label="规格" min-width="120" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="unit_qty" label="单机用量" width="80" align="right"></el-table-column>
|
||||
<el-table-column prop="total_demand_qty" label="需求总量" width="90" align="right"></el-table-column>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="发料异常比对" align="center">
|
||||
<el-table-column prop="warehouse_issue_qty" label="仓库发放数量" width="100" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span style="font-weight: bold;" v-text="scope.row.warehouse_issue_qty"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="theoretical_issue_qty" label="理论出料数量" width="100" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span style="color: #909399;" v-text="scope.row.theoretical_issue_qty"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="差异" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span :style="{
|
||||
color: (scope.row.warehouse_issue_qty - scope.row.theoretical_issue_qty) > 0 ? '#F56C6C' : '#67C23A',
|
||||
fontWeight: 'bold'
|
||||
}" v-text="(scope.row.warehouse_issue_qty - scope.row.theoretical_issue_qty).toFixed(2)">
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="issue_status" label="发料情况" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.issue_status === '超领发料' ? 'danger' : (scope.row.issue_status === '未发料' ? 'info' : 'warning')" size="small" v-text="scope.row.issue_status">
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="currentPage"
|
||||
:page-sizes="[20, 50, 100, 200]"
|
||||
:page-size="pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
isSystemBusy: false,
|
||||
globalTaskName: "",
|
||||
statusTimer: null,
|
||||
showLogDialog: false,
|
||||
taskLogs: [],
|
||||
logTimer: null,
|
||||
tableData: [],
|
||||
total: 0,
|
||||
currentPage: 1,
|
||||
pageSize: 50,
|
||||
searchParams: {
|
||||
work_orders_number: '',
|
||||
material_code: '',
|
||||
issue_status: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchData();
|
||||
this.checkTaskStatus();
|
||||
this.statusTimer = setInterval(this.checkTaskStatus, 3000);
|
||||
this.logTimer = setInterval(this.fetchLogs, 1000);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.statusTimer) {
|
||||
clearInterval(this.statusTimer);
|
||||
}
|
||||
if (this.logTimer) {
|
||||
clearInterval(this.logTimer);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchLogs() {
|
||||
if (this.isSystemBusy && this.showLogDialog) {
|
||||
axios.get('/api/task_logs')
|
||||
.then(res => {
|
||||
this.taskLogs = res.data.logs;
|
||||
this.$nextTick(() => {
|
||||
const container = document.getElementById('log-container');
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(err => {});
|
||||
}
|
||||
},
|
||||
checkTaskStatus() {
|
||||
axios.get('/api/task_status')
|
||||
.then(res => {
|
||||
this.isSystemBusy = res.data.is_busy;
|
||||
this.globalTaskName = res.data.task_name;
|
||||
})
|
||||
.catch(err => {});
|
||||
},
|
||||
goBack() {
|
||||
window.location.href = '/';
|
||||
},
|
||||
fetchData() {
|
||||
this.loading = true;
|
||||
|
||||
const params = {
|
||||
page: this.currentPage,
|
||||
limit: this.pageSize,
|
||||
work_orders_number: this.searchParams.work_orders_number,
|
||||
material_code: this.searchParams.material_code,
|
||||
issue_status: this.searchParams.issue_status
|
||||
};
|
||||
|
||||
axios.get('/api/abnormal_report', { params })
|
||||
.then(res => {
|
||||
this.tableData = res.data.rows;
|
||||
this.total = res.data.total;
|
||||
})
|
||||
.catch(err => {
|
||||
this.$message.error('获取数据失败');
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
handleSearch() {
|
||||
this.currentPage = 1;
|
||||
this.fetchData();
|
||||
},
|
||||
resetSearch() {
|
||||
this.searchParams.work_orders_number = '';
|
||||
this.searchParams.material_code = '';
|
||||
this.searchParams.issue_status = '';
|
||||
this.handleSearch();
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.fetchData();
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
this.currentPage = val;
|
||||
this.fetchData();
|
||||
},
|
||||
syncAbnormalReport() {
|
||||
this.$confirm('确定要抓取发料异常报表吗?该操作会在后台打开 ERP 进行翻页抓取。', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.isSystemBusy = true; // 立即将状态置为 busy,展示 loading
|
||||
axios.post('/api/sync_abnormal_report')
|
||||
.then(res => {
|
||||
if (res.data.success) {
|
||||
this.$message.success(res.data.message);
|
||||
setTimeout(this.checkTaskStatus, 500);
|
||||
// 自动弹开日志窗口让用户看进度
|
||||
this.showLogDialog = true;
|
||||
} else {
|
||||
this.$message.error(res.data.message);
|
||||
this.isSystemBusy = false;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.response && err.response.status === 409) {
|
||||
this.$message.warning(err.response.data.message);
|
||||
} else {
|
||||
this.$message.error(err.response?.data?.message || '触发抓取异常报表失败');
|
||||
}
|
||||
this.isSystemBusy = false;
|
||||
console.error(err);
|
||||
});
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -104,6 +104,12 @@
|
||||
.card-bom-compare:hover { border-color: #E6A23C; background-color: #fdf6ec; }
|
||||
.card-bom-compare 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
171
web_ui/templates/work_orders.html
Normal file
171
web_ui/templates/work_orders.html
Normal file
@@ -0,0 +1,171 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>生产工单明细看板</title>
|
||||
<!-- 引入 ElementUI 样式 -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
|
||||
<!-- 引入 Vue.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
|
||||
<!-- 引入 ElementUI 组件库 -->
|
||||
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
|
||||
<!-- 引入 axios -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<style>
|
||||
body { margin: 0; padding: 20px; font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif; background-color: #f0f2f5; }
|
||||
.box-card { margin-bottom: 20px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.page-header h2 { margin: 0; color: #303133; }
|
||||
.search-row { display: flex; gap: 15px; margin-bottom: 20px; }
|
||||
.pagination-container { margin-top: 20px; text-align: right; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<el-card class="box-card">
|
||||
<div class="page-header">
|
||||
<h2><i class="el-icon-document" style="margin-right: 10px; color: #E6A23C;"></i>生产工单明细</h2>
|
||||
<div>
|
||||
<el-button type="info" plain icon="el-icon-back" @click="goBack" size="small">返回主控台</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-row">
|
||||
<el-input v-model="searchParams.work_orders_number" placeholder="工单号 (支持模糊搜索)" style="width: 250px" clearable @clear="handleSearch" @keyup.enter.native="handleSearch">
|
||||
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||
</el-input>
|
||||
<el-input v-model="searchParams.material_name" placeholder="物料名称 (支持模糊搜索)" style="width: 250px" clearable @clear="handleSearch" @keyup.enter.native="handleSearch">
|
||||
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||
</el-input>
|
||||
<el-input v-model="searchParams.material_code" placeholder="物料代码 (支持模糊搜索)" style="width: 250px" clearable @clear="handleSearch" @keyup.enter.native="handleSearch">
|
||||
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||
</el-input>
|
||||
<el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh-left" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="tableData" v-loading="loading" border style="width: 100%" stripe size="small" :header-cell-style="{background:'#f5f7fa',color:'#606266'}">
|
||||
<el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
|
||||
<el-table-column prop="execution_time" label="执行时间" width="160" sortable></el-table-column>
|
||||
<el-table-column prop="work_orders_number" label="工单号" width="160"></el-table-column>
|
||||
<el-table-column prop="line_number" label="行号" width="60" align="center"></el-table-column>
|
||||
<el-table-column prop="material_code" label="物料代码" width="120"></el-table-column>
|
||||
<el-table-column prop="material_name" label="物料名称" min-width="150" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="material_specification" label="规格" min-width="150" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="warehouse_name" label="仓库" width="120" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="数量信息" align="center">
|
||||
<el-table-column prop="issue_number" label="应发数量" width="90" align="right"></el-table-column>
|
||||
<el-table-column prop="total_issue_number" label="已发料数量" width="100" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span :style="{color: scope.row.total_issue_number >= scope.row.issue_number ? '#67C23A' : '#F56C6C', fontWeight: 'bold'}" v-text="scope.row.total_issue_number">
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="unit_name" label="单位" width="60" align="center"></el-table-column>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额信息" align="center">
|
||||
<el-table-column prop="cost_price" label="成本单价" width="100" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span v-text="'¥ ' + Number(scope.row.cost_price || 0).toFixed(2)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="issue_amount_total" label="发料总额" width="100" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span style="color: #409EFF; font-weight: bold;" v-text="'¥ ' + Number(scope.row.issue_amount_total || 0).toFixed(2)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
<el-table-column prop="executor_user_name" label="执行人" width="100" align="center"></el-table-column>
|
||||
<el-table-column prop="materials_user_name" label="领料人" width="100" align="center"></el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="currentPage"
|
||||
:page-sizes="[20, 50, 100, 200]"
|
||||
:page-size="pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
syncingReport: false,
|
||||
tableData: [],
|
||||
total: 0,
|
||||
currentPage: 1,
|
||||
pageSize: 50,
|
||||
searchParams: {
|
||||
work_orders_number: '',
|
||||
material_name: '',
|
||||
material_code: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchData();
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
window.location.href = '/';
|
||||
},
|
||||
fetchData() {
|
||||
this.loading = true;
|
||||
|
||||
const params = {
|
||||
page: this.currentPage,
|
||||
limit: this.pageSize,
|
||||
work_orders_number: this.searchParams.work_orders_number,
|
||||
material_name: this.searchParams.material_name,
|
||||
material_code: this.searchParams.material_code
|
||||
};
|
||||
|
||||
axios.get('/api/work_orders', { params })
|
||||
.then(res => {
|
||||
this.tableData = res.data.rows;
|
||||
this.total = res.data.total;
|
||||
})
|
||||
.catch(err => {
|
||||
this.$message.error('获取数据失败');
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
handleSearch() {
|
||||
this.currentPage = 1;
|
||||
this.fetchData();
|
||||
},
|
||||
resetSearch() {
|
||||
this.searchParams.work_orders_number = '';
|
||||
this.searchParams.material_name = '';
|
||||
this.searchParams.material_code = '';
|
||||
this.handleSearch();
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.fetchData();
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
this.currentPage = val;
|
||||
this.fetchData();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user