408 lines
16 KiB
HTML
408 lines
16 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>ERP 成本与数据看板 - 主控台</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 {
|
||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
||
margin: 0;
|
||
padding: 0;
|
||
background-color: #f0f2f5;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
height: 100vh;
|
||
}
|
||
|
||
.dashboard-container {
|
||
background-color: #ffffff;
|
||
border-radius: 12px;
|
||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||
padding: 40px;
|
||
width: 80%;
|
||
max-width: 900px;
|
||
text-align: center;
|
||
}
|
||
|
||
.header-title {
|
||
color: #303133;
|
||
font-size: 28px;
|
||
margin-bottom: 10px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.header-subtitle {
|
||
color: #909399;
|
||
font-size: 16px;
|
||
margin-bottom: 40px;
|
||
}
|
||
|
||
.nav-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 30px;
|
||
}
|
||
|
||
.nav-card {
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 8px;
|
||
padding: 30px 20px;
|
||
cursor: pointer;
|
||
transition: all 0.3s cubic-bezier(.25,.8,.25,1);
|
||
background-color: #fafafa;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.nav-card:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 10px 20px rgba(0,0,0,0.08);
|
||
border-color: #409EFF;
|
||
background-color: #ecf5ff;
|
||
}
|
||
|
||
.nav-card i {
|
||
font-size: 48px;
|
||
color: #409EFF;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.nav-card h3 {
|
||
margin: 0 0 10px 0;
|
||
color: #303133;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.nav-card p {
|
||
margin: 0;
|
||
color: #606266;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* 特定卡片的颜色定制 */
|
||
.card-receipt { border-top: 4px solid #67C23A; }
|
||
.card-receipt i { color: #67C23A; }
|
||
.card-receipt:hover { border-color: #67C23A; background-color: #f0f9eb; }
|
||
|
||
.card-bom-tree { border-top: 4px solid #409EFF; }
|
||
.card-bom-tree i { color: #409EFF; }
|
||
.card-bom-tree:hover { border-color: #409EFF; background-color: #ecf5ff; }
|
||
|
||
.card-bom-compare { border-top: 4px solid #9c27b0; }
|
||
.card-bom-compare i { color: #9c27b0; }
|
||
.card-bom-compare:hover { border-color: #9c27b0; background-color: #f3e5f5; }
|
||
|
||
.card-work-order { border-top: 4px solid #E6A23C; }
|
||
.card-work-order i { color: #E6A23C; }
|
||
.card-work-order:hover { border-color: #E6A23C; background-color: #fdf6ec; }
|
||
|
||
.card-abnormal { border-top: 4px solid #F56C6C; }
|
||
.card-abnormal i { color: #F56C6C; }
|
||
.card-abnormal:hover { border-color: #F56C6C; background-color: #fef0f0; }
|
||
|
||
.card-reconciliation { border-top: 4px solid #909399; }
|
||
.card-reconciliation i { color: #909399; }
|
||
.card-reconciliation:hover { border-color: #909399; background-color: #f4f4f5; }
|
||
|
||
.action-group {
|
||
margin-top: 40px;
|
||
padding-top: 30px;
|
||
border-top: 1px dashed #ebeef5;
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 20px;
|
||
position: relative;
|
||
}
|
||
|
||
.log-window {
|
||
margin-top: 30px;
|
||
background-color: #1e1e1e;
|
||
color: #a9b7c6;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
height: 250px;
|
||
overflow-y: auto;
|
||
text-align: left;
|
||
font-family: 'Consolas', 'Courier New', monospace;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
box-shadow: inset 0 0 10px rgba(0,0,0,0.5);
|
||
border: 1px solid #333;
|
||
}
|
||
|
||
.log-window::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
.log-window::-webkit-scrollbar-track {
|
||
background: #1e1e1e;
|
||
}
|
||
.log-window::-webkit-scrollbar-thumb {
|
||
background: #555;
|
||
border-radius: 4px;
|
||
}
|
||
.log-window::-webkit-scrollbar-thumb:hover {
|
||
background: #777;
|
||
}
|
||
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="dashboard-container" id="app">
|
||
<div class="header-title">ERP 数据分析与成本管控台</div>
|
||
<div class="header-subtitle">请选择您需要进入的业务模块</div>
|
||
|
||
<div class="nav-grid">
|
||
<!-- 卡片 1: 收货明细表 -->
|
||
<div class="nav-card card-receipt" onclick="window.location.href='/receipts'">
|
||
<i class="el-icon-document"></i>
|
||
<h3>财务收货明细报表</h3>
|
||
<p>查看、搜索所有历史财务收货记录及详细价格数据。</p>
|
||
</div>
|
||
|
||
<!-- 卡片 2: BOM 雷达图 -->
|
||
<div class="nav-card card-bom-tree" onclick="window.location.href='/bom'">
|
||
<i class="el-icon-share"></i>
|
||
<h3>BOM 成本雷达图</h3>
|
||
<p>以关系图谱形式可视化展示产品的层级结构与汇总成本。</p>
|
||
</div>
|
||
|
||
<!-- 卡片 3: 期间成本对比 -->
|
||
<div class="nav-card card-bom-compare" onclick="window.location.href='/compare'">
|
||
<i class="el-icon-data-line"></i>
|
||
<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>
|
||
|
||
<!-- 卡片 6: 绩效核查与BOM比对 -->
|
||
<div class="nav-card card-reconciliation" onclick="window.location.href='/reconciliation'">
|
||
<i class="el-icon-data-analysis"></i>
|
||
<h3>绩效核查与BOM比对</h3>
|
||
<p>数据清洗、匹配工单号,智能比对 BOM 理论发料量与实际发料量差异。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<div v-if="globalTaskName" style="position: absolute; top: -30px; width: 100%; text-align: center; color: #E6A23C; font-weight: bold; font-size: 14px;">
|
||
<i class="el-icon-loading"></i> 系统忙碌中:正在执行 {{ globalTaskName }},在此期间无法发起新的抓取。
|
||
</div>
|
||
|
||
<el-button
|
||
type="success"
|
||
:icon="isSystemBusy ? 'el-icon-loading' : 'el-icon-refresh'"
|
||
:disabled="isSystemBusy"
|
||
@click="syncReceipts"
|
||
round>
|
||
<span v-text="syncing ? '请求已发送...' : '读取最新财务收货明细报表'"></span>
|
||
</el-button>
|
||
|
||
<el-button
|
||
type="primary"
|
||
:icon="isSystemBusy ? 'el-icon-loading' : 'el-icon-refresh-right'"
|
||
:disabled="isSystemBusy"
|
||
@click="syncBom"
|
||
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>
|
||
|
||
<el-button
|
||
type="info"
|
||
:icon="isSystemBusy ? 'el-icon-loading' : 'el-icon-refresh'"
|
||
:disabled="isSystemBusy"
|
||
@click="syncIssueReceipts"
|
||
round>
|
||
<span v-text="syncingIssueReceipts ? '请求已发送...' : '读取发料单明细'"></span>
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
{% include 'global_log.html' %}
|
||
|
||
<script>
|
||
new Vue({
|
||
el: '#app',
|
||
data() {
|
||
return {
|
||
syncing: false,
|
||
syncingBom: false,
|
||
syncingWorkOrders: false,
|
||
syncingIssueReceipts: false,
|
||
isSystemBusy: false,
|
||
globalTaskName: "",
|
||
statusTimer: null
|
||
}
|
||
},
|
||
mounted() {
|
||
// 页面加载时立刻检查一次
|
||
this.checkTaskStatus();
|
||
// 之后每隔 3 秒轮询一次后端状态
|
||
this.statusTimer = setInterval(this.checkTaskStatus, 3000);
|
||
},
|
||
beforeDestroy() {
|
||
if (this.statusTimer) {
|
||
clearInterval(this.statusTimer);
|
||
}
|
||
},
|
||
methods: {
|
||
checkTaskStatus() {
|
||
axios.get('/api/task_status')
|
||
.then(res => {
|
||
this.isSystemBusy = res.data.is_busy;
|
||
this.globalTaskName = res.data.task_name;
|
||
})
|
||
.catch(err => {
|
||
// 忽略检查错误
|
||
});
|
||
},
|
||
syncReceipts() {
|
||
this.syncing = true;
|
||
if (window.globalLogApp) {
|
||
window.globalLogApp.logDialogVisible = true;
|
||
}
|
||
axios.post('/api/sync_receipts')
|
||
.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.syncing = false;
|
||
});
|
||
},
|
||
syncBom() {
|
||
this.$confirm('此操作将启动后台浏览器,耗时较长(约10-20分钟),期间请勿关闭服务器终端。确认执行?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(() => {
|
||
this.syncingBom = true;
|
||
// 如果存在全局日志组件,自动打开日志面板
|
||
if (window.globalLogApp) {
|
||
window.globalLogApp.logDialogVisible = true;
|
||
}
|
||
axios.post('/api/sync_bom')
|
||
.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.syncingBom = false;
|
||
});
|
||
}).catch(() => {});
|
||
},
|
||
syncWorkOrders() {
|
||
this.syncingWorkOrders = true;
|
||
if (window.globalLogApp) {
|
||
window.globalLogApp.logDialogVisible = 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;
|
||
});
|
||
},
|
||
syncIssueReceipts() {
|
||
this.syncingIssueReceipts = true;
|
||
if (window.globalLogApp) {
|
||
window.globalLogApp.logDialogVisible = true;
|
||
}
|
||
axios.post('/api/sync_issue_receipts')
|
||
.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.syncingIssueReceipts = false;
|
||
});
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|