feat: MES Dashboard 初始版本

- 整合 WIP 報表、機台狀態報表、數據表查詢工具
- 機台狀態報表功能:
  - 支援廠區/資產狀態多選篩選
  - 工站卡片顯示 OU%、機台數、狀態條圖
  - 條圖支援滑鼠懸停顯示詳細資訊
  - 工站合併分組 (切割、焊接、成型等)
- 優化 filter_options API 查詢效能
- 統一狀態顏色定義 (PRD/SBY/UDT/SDT/EGT/NST)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
ymirliu
2026-01-15 19:21:53 +08:00
commit 313e41390b
25 changed files with 21522 additions and 0 deletions

31
.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
ENV/
env/
.venv/
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
nul
# Logs
*.log
# Local config
.env
*.local
# Claude
.claude/

253
README.md Normal file
View File

@@ -0,0 +1,253 @@
# MES 報表查詢系統
基於 Vite/React + Python FastAPI 的 MES 數據報表查詢與可視化系統
---
## 專案狀態
- ✅ 數據庫分析完成
- ✅ 系統架構設計完成
- ✅ 數據查詢工具完成
- ⏳ 待提供 Power BI 報表設計參考
- ⏳ 系統開發進行中
---
## 快速開始
### 1. WIP 在制品報表(當前可用)⭐
查詢當前在制品的數量統計,支援按工序、工作中心、產品線分組查看。
```bash
# 雙擊運行
scripts\啟動Dashboard.bat
```
然後訪問: **http://localhost:5000**
入口頁面可用上方 Tab 切換「WIP 報表 / 數據表查詢工具」。
**功能**:
- 📊 總覽統計(總 LOT 數、總數量、總片數)
- 🔍 按 SPEC 和 WORKCENTER 統計
- 📈 按產品線統計(匯總 + 明細)
- ⏱️ 可選時間範圍1-30 天)
- 🎨 美觀的 Web UI
詳細說明: [WIP報表說明.md](docs/WIP報表說明.md)
---
### 2. 查看數據表內容(當前可用)
#### 方法 A: 自動初始化(推薦,首次使用)
```bash
# 步驟 1: 初始化環境(只需執行一次)
雙擊運行: scripts\0_初始化環境.bat
# 步驟 2: 啟動服務器
雙擊運行: scripts\啟動Dashboard.bat
```
#### 方法 B: 使用 Python 直接啟動
```bash
# 如果您的環境已安裝 Flask, Pandas, oracledb
python apps\快速啟動.py
```
#### 方法 C: 手動啟動
```bash
# 1. 創建虛擬環境(首次)
python -m venv venv
# 2. 安裝依賴(首次)
venv\Scripts\pip.exe install -r requirements.txt
# 3. 啟動服務器
venv\Scripts\python.exe apps\portal.py
```
然後訪問: **http://localhost:5000**
**功能**:
- 📊 按表性質分類(現況表/歷史表/輔助表)
- 🔍 查看各表最後 1000 筆資料
- ⏱️ 大表自動按時間欄位排序
- 📋 顯示欄位列表和數據樣本
---
## 文檔結構
### 核心文檔
| 文檔 | 用途 | 適用對象 |
|------|------|---------|
| **[System_Architecture_Design.md](docs/System_Architecture_Design.md)** | 系統架構設計完整文檔 | 架構師、開發者 |
| **[MES_Core_Tables_Analysis_Report.md](docs/MES_Core_Tables_Analysis_Report.md)** | 核心表深度分析報告 ⭐ | 開發者、數據分析師 |
| **[MES_Database_Reference.md](docs/MES_Database_Reference.md)** | 數據庫完整結構參考 | 開發者 |
### 文檔關係
```
docs/System_Architecture_Design.md (系統設計總覽)
↓ 引用
docs/MES_Core_Tables_Analysis_Report.md (表詳細分析)
↓ 引用
docs/MES_Database_Reference.md (表結構參考)
```
---
## 關鍵發現總結
### 1. 表性質分類
經過深入分析16 張核心表分為:
- **現況快照表4張**: WIP, RESOURCE, CONTAINER, JOB
- **歷史累積表10張**: RESOURCESTATUS, LOTWIPHISTORY 等
- **輔助表2張**: PARTREQUESTORDER, PJ_COMBINEDASSYLOTS
### 2. 重要認知更新
⚠️ **DW_MES_WIP** 雖名為"在制品表",但實際包含 **7700 萬行歷史累積數據**
⚠️ **DW_MES_RESOURCESTATUS** 記錄設備狀態每次變更,需用兩個時間欄位計算持續時間:
```sql
狀態持續時間 = (LASTSTATUSCHANGEDATE - OLDLASTSTATUSCHANGEDATE) * 24 小時
```
### 3. 查詢優化鐵律
**所有超過 1000 萬行的表,查詢時必須加入時間範圍限制!**
```sql
-- DW_MES_WIP (7700萬行)
WHERE TXNDATE >= TRUNC(SYSDATE) - 7
-- DW_MES_RESOURCESTATUS (6500萬行)
WHERE OLDLASTSTATUSCHANGEDATE >= TRUNC(SYSDATE) - 7
-- DW_MES_LOTWIPHISTORY (5300萬行)
WHERE TRACKINTIMESTAMP >= TRUNC(SYSDATE) - 7
```
**建議時間範圍**:
- 儀表板查詢: 最近 **7 天**
- 報表查詢: 最多 **30 天**
- 歷史趨勢: 最多 **90 天**
---
## 核心業務場景
基於表分析,系統應重點支援:
1.**在制品WIP看板** - 使用 DW_MES_WIP
2.**設備稼動率OEE報表** - 使用 DW_MES_RESOURCESTATUS
3.**批次生產履歷追溯** - 使用 DW_MES_LOTWIPHISTORY
4.**工序 Cycle Time 分析** - 使用 DW_MES_LOTWIPHISTORY
5.**設備產出與效率分析** - 使用 DW_MES_HM_LOTMOVEOUT
6.**Hold 批次分析** - 使用 DW_MES_WIP + DW_MES_HOLDRELEASEHISTORY
7.**設備維修工單進度追蹤** - 使用 DW_MES_JOB
8.**良率分析** - 使用 DW_MES_LOTREJECTHISTORY
---
## 技術架構
### 前端技術棧
- React 18 + TypeScript
- Vite 5.x (構建工具)
- Ant Design 5.x (UI 組件庫)
- ECharts 5.x (圖表庫)
- React Query 5.x (數據管理)
### 後端技術棧
- Python 3.11+
- FastAPI (Web 框架)
- oracledb 2.x (Oracle 驅動)
- Pandas 2.x (數據處理)
### 數據庫
- Oracle Database 19c Enterprise Edition
- 主機: 10.1.1.58:1521
- 服務名: DWDB
- 用戶: MBU1_R (只讀)
---
## 開發計劃
### Phase 1: 環境搭建與基礎架構 ⏳
- [ ] 初始化 FastAPI 項目
- [ ] 初始化 Vite + React 項目
- [ ] 建立數據庫連接池
- [ ] 實現基礎 API 結構
### Phase 2: 儀表板開發 ⏳
- [ ] 實現儀表板 API
- [ ] 開發儀表板前端頁面
- [ ] 實現圖表組件
### Phase 3: 報表查詢模塊開發 ⏳
待 Power BI 截圖確認
### Phase 4: 匯出功能開發 ⏳
- [ ] 實現 Excel 匯出
- [ ] 實現異步匯出
### Phase 5: 優化與測試 ⏳
- [ ] 性能優化
- [ ] 測試
### Phase 6: 部署上線 ⏳
- [ ] 準備部署環境
- [ ] 部署
---
## 專案文件
```
DashBoard/
├── README.md # 本文件
├── docs/ # 專案文檔
├── scripts/ # 啟動腳本
├── apps/ # 可執行應用
│ └── templates/ # Web UI 模板
├── tools/ # 工具腳本
├── data/ # 產出資料
├── requirements.txt # Python 依賴
├── venv/ # Python 虛擬環境
├── backend/ # 後端(待開發)
└── frontend/ # 前端(待開發)
```
---
## 待確認事項
1.**Power BI 報表截圖** - 用於前端 UI 設計參考
2.**具體報表類型** - 從 8 個業務場景中選擇優先開發的 3-5 個
3.**部署環境** - 是否有專用服務器,是否使用 Docker
4.**並發用戶數** - 預計同時使用的用戶數量
---
## 聯絡方式
如有技術問題或需求變更,請及時更新相關文檔。
---
**文檔版本**: 1.0
**最後更新**: 2026-01-14

1536
apps/portal.py Normal file

File diff suppressed because it is too large Load Diff

270
apps/table_data_viewer.py Normal file
View File

@@ -0,0 +1,270 @@
"""
MES 數據表查詢工具
用於查看各數據表的最後 1000 筆資料,確認表結構和內容
"""
import oracledb
import pandas as pd
from flask import Flask, render_template, request, jsonify
from datetime import datetime
import json
# 數據庫連接配置
DB_CONFIG = {
'user': 'MBU1_R',
'password': 'Pj2481mbu1',
'dsn': '10.1.1.58:1521/DWDB'
}
# 16 張核心表配置(含表性質分類)
TABLES_CONFIG = {
'現況快照表': [
{
'name': 'DW_MES_WIP',
'display_name': 'WIP (在制品表)',
'row_count': 77470834,
'time_field': 'TXNDATE',
'description': '在製品現況表(含歷史累積)- 當前 WIP 狀態/數量'
},
{
'name': 'DW_MES_RESOURCE',
'display_name': 'RESOURCE (資源主檔)',
'row_count': 90620,
'time_field': None,
'description': '資源表 - 設備/載具等資源基本資料OBJECTCATEGORY=ASSEMBLY 時RESOURCENAME 為設備編號)'
},
{
'name': 'DW_MES_CONTAINER',
'display_name': 'CONTAINER (容器信息表)',
'row_count': 5185532,
'time_field': 'LASTMOVEOUTTIMESTAMP',
'description': '容器/批次主檔 - 目前在製容器狀態、數量與流程資訊'
},
{
'name': 'DW_MES_JOB',
'display_name': 'JOB (設備維修工單)',
'row_count': 1239659,
'time_field': 'CREATEDATE',
'description': '設備維修工單表 - 維修工單的當前狀態與流程'
}
],
'歷史累積表': [
{
'name': 'DW_MES_RESOURCESTATUS',
'display_name': 'RESOURCESTATUS (資源狀態歷史) ⭐',
'row_count': 65139825,
'time_field': 'OLDLASTSTATUSCHANGEDATE',
'description': '設備狀態變更歷史表 - 狀態切換與原因'
},
{
'name': 'DW_MES_RESOURCESTATUS_SHIFT',
'display_name': 'RESOURCESTATUS_SHIFT (資源班次狀態)',
'row_count': 74155046,
'time_field': 'SHIFTDATE',
'description': '設備狀態班次彙總表 - 班次級狀態/工時'
},
{
'name': 'DW_MES_LOTWIPHISTORY',
'display_name': 'LOTWIPHISTORY (批次流轉歷史) ⭐',
'row_count': 53085425,
'time_field': 'TRACKINTIMESTAMP',
'description': '在製流轉歷史表 - 批次進出站與流程軌跡'
},
{
'name': 'DW_MES_LOTWIPDATAHISTORY',
'display_name': 'LOTWIPDATAHISTORY (批次數據歷史)',
'row_count': 77168503,
'time_field': 'TXNTIMESTAMP',
'description': '在製數據採集歷史表 - 製程量測/參數紀錄'
},
{
'name': 'DW_MES_HM_LOTMOVEOUT',
'display_name': 'HM_LOTMOVEOUT (批次移出表)',
'row_count': 48374309,
'time_field': 'TXNDATE',
'description': '批次出站事件歷史表 - 出站/移出交易'
},
{
'name': 'DW_MES_JOBTXNHISTORY',
'display_name': 'JOBTXNHISTORY (維修工單交易歷史)',
'row_count': 9488096,
'time_field': 'TXNDATE',
'description': '維修工單交易歷史表 - 工單狀態變更紀錄'
},
{
'name': 'DW_MES_LOTREJECTHISTORY',
'display_name': 'LOTREJECTHISTORY (批次拒絕歷史)',
'row_count': 15678513,
'time_field': 'CREATEDATE',
'description': '批次不良/報廢歷史表 - 不良原因與數量'
},
{
'name': 'DW_MES_LOTMATERIALSHISTORY',
'display_name': 'LOTMATERIALSHISTORY (物料消耗歷史)',
'row_count': 17702828,
'time_field': 'CREATEDATE',
'description': '批次物料消耗歷史表 - 用料與批次關聯'
},
{
'name': 'DW_MES_HOLDRELEASEHISTORY',
'display_name': 'HOLDRELEASEHISTORY (Hold/Release歷史)',
'row_count': 310033,
'time_field': 'HOLDTXNDATE',
'description': 'Hold/Release 歷史表 - 批次停工與解除紀錄'
},
{
'name': 'DW_MES_MAINTENANCE',
'display_name': 'MAINTENANCE (設備維護歷史)',
'row_count': 50954850,
'time_field': 'CREATEDATE',
'description': '設備保養/維護紀錄表 - 保養計畫與點檢數據'
}
],
'輔助表': [
{
'name': 'DW_MES_PARTREQUESTORDER',
'display_name': 'PARTREQUESTORDER (物料請求訂單)',
'row_count': 61396,
'time_field': None,
'description': '維修用料請求表 - 維修/設備零件請領'
},
{
'name': 'DW_MES_PJ_COMBINEDASSYLOTS',
'display_name': 'PJ_COMBINEDASSYLOTS (組合裝配批次)',
'row_count': 1955691,
'time_field': None,
'description': '併批紀錄表 - 合批/合併批次關聯與數量資訊'
}
]
}
app = Flask(__name__)
def get_db_connection():
"""建立數據庫連接"""
try:
connection = oracledb.connect(**DB_CONFIG)
return connection
except Exception as e:
print(f"數據庫連接失敗: {e}")
return None
def get_table_data(table_name, limit=1000, time_field=None):
"""
查詢表的最後 N 筆資料
Args:
table_name: 表名
limit: 返回行數
time_field: 時間欄位(用於排序)
Returns:
dict: 包含 columns, data, row_count 的字典
"""
connection = get_db_connection()
if not connection:
return {'error': '數據庫連接失敗'}
try:
cursor = connection.cursor()
# 構建查詢 SQL
if time_field:
# 如果有時間欄位,按時間倒序
sql = f"""
SELECT * FROM (
SELECT * FROM {table_name}
WHERE {time_field} IS NOT NULL
ORDER BY {time_field} DESC
) WHERE ROWNUM <= {limit}
"""
else:
# 沒有時間欄位,直接取前 N 筆
sql = f"""
SELECT * FROM {table_name}
WHERE ROWNUM <= {limit}
"""
# 執行查詢
cursor.execute(sql)
# 獲取欄位名
columns = [desc[0] for desc in cursor.description]
# 獲取數據
rows = cursor.fetchall()
# 轉換為 JSON 可序列化格式
data = []
for row in rows:
row_dict = {}
for i, col in enumerate(columns):
value = row[i]
# 處理日期類型
if isinstance(value, datetime):
row_dict[col] = value.strftime('%Y-%m-%d %H:%M:%S')
# 處理 None
elif value is None:
row_dict[col] = None
# 處理數字
elif isinstance(value, (int, float)):
row_dict[col] = value
# 其他轉為字符串
else:
row_dict[col] = str(value)
data.append(row_dict)
cursor.close()
connection.close()
return {
'columns': columns,
'data': data,
'row_count': len(data)
}
except Exception as e:
if connection:
connection.close()
return {'error': f'查詢失敗: {str(e)}'}
@app.route('/')
def index():
"""首頁 - 顯示所有表列表"""
return render_template('index.html', tables_config=TABLES_CONFIG)
@app.route('/api/query_table', methods=['POST'])
def query_table():
"""API: 查詢指定表的資料"""
data = request.get_json()
table_name = data.get('table_name')
limit = data.get('limit', 1000)
time_field = data.get('time_field')
if not table_name:
return jsonify({'error': '請指定表名'}), 400
result = get_table_data(table_name, limit, time_field)
return jsonify(result)
@app.route('/api/get_table_info', methods=['GET'])
def get_table_info():
"""API: 獲取所有表的配置信息"""
return jsonify(TABLES_CONFIG)
if __name__ == '__main__':
# 檢查數據庫連接
print("正在測試數據庫連接...")
conn = get_db_connection()
if conn:
print("✓ 數據庫連接成功!")
conn.close()
print("\n啟動 Web 服務器...")
print("請訪問: http://localhost:5000")
print("\n提示:")
print("- 點擊表名查看最後 1000 筆資料")
print("- 大表會自動使用時間欄位排序")
print("- 按 Ctrl+C 停止服務器")
app.run(debug=True, host='0.0.0.0', port=5000)
else:
print("✗ 數據庫連接失敗,請檢查配置")

606
apps/templates/index.html Normal file
View File

@@ -0,0 +1,606 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MES 數據表查詢工具</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft JhengHei', Arial, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 8px 8px 0 0;
}
.header h1 {
font-size: 28px;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
font-size: 14px;
}
.content {
padding: 30px;
}
.table-category {
margin-bottom: 30px;
}
.category-title {
font-size: 20px;
font-weight: bold;
color: #333;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
}
.table-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 15px;
}
.table-card {
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 15px;
cursor: pointer;
transition: all 0.3s;
background: #fafafa;
}
.table-card:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
transform: translateY(-2px);
}
.table-card.active {
border-color: #667eea;
background: #f0f4ff;
}
.table-name {
font-size: 16px;
font-weight: bold;
color: #667eea;
margin-bottom: 8px;
}
.table-info {
font-size: 13px;
color: #666;
margin-bottom: 5px;
}
.table-desc {
font-size: 12px;
color: #999;
margin-top: 8px;
font-style: italic;
}
.data-viewer {
margin-top: 30px;
display: none;
}
.data-viewer.active {
display: block;
}
.viewer-header {
background: #667eea;
color: white;
padding: 15px 20px;
border-radius: 6px 6px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.viewer-header h3 {
font-size: 18px;
}
.close-btn {
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.close-btn:hover {
background: rgba(255,255,255,0.3);
}
.table-container {
overflow-x: auto;
max-height: 600px;
border: 1px solid #e0e0e0;
border-top: none;
border-radius: 0 0 6px 6px;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
thead {
position: sticky;
top: 0;
background: #f8f9fa;
z-index: 10;
}
th {
padding: 12px 10px;
text-align: left;
border-bottom: 2px solid #dee2e6;
font-weight: 600;
color: #495057;
white-space: nowrap;
}
td {
padding: 10px;
border-bottom: 1px solid #f0f0f0;
color: #333;
}
tr:hover {
background: #f8f9fa;
}
.loading {
text-align: center;
padding: 40px;
color: #667eea;
font-size: 16px;
}
.error {
background: #fee;
color: #c33;
padding: 20px;
border-radius: 6px;
margin-top: 20px;
}
.badge {
display: inline-block;
background: #667eea;
color: white;
padding: 2px 8px;
border-radius: 3px;
font-size: 11px;
margin-left: 5px;
}
.badge.large {
background: #dc3545;
}
.stats {
display: flex;
gap: 20px;
margin-bottom: 15px;
padding: 15px;
background: #f8f9fa;
border-radius: 6px;
flex-wrap: wrap;
align-items: center;
}
.stat-item {
display: flex;
flex-direction: column;
}
.stat-label {
font-size: 12px;
color: #666;
margin-bottom: 5px;
}
.stat-value {
font-size: 18px;
font-weight: bold;
color: #667eea;
}
.filter-row input {
width: 100%;
padding: 6px 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 12px;
box-sizing: border-box;
}
.filter-row input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.filter-row input::placeholder {
color: #aaa;
}
.filter-row th {
padding: 8px 10px;
background: #e9ecef;
border-bottom: 1px solid #dee2e6;
}
.query-btn {
background: #667eea;
color: white;
border: none;
padding: 10px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
margin-left: auto;
}
.query-btn:hover {
background: #5a6fd6;
}
.query-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.clear-btn {
background: #6c757d;
color: white;
border: none;
padding: 10px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-left: 10px;
}
.clear-btn:hover {
background: #5a6268;
}
.filter-hint {
font-size: 12px;
color: #666;
margin-left: 15px;
}
.active-filters {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.filter-tag {
background: #e3e8ff;
color: #667eea;
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
display: flex;
align-items: center;
gap: 5px;
}
.filter-tag .remove {
cursor: pointer;
font-weight: bold;
}
.filter-tag .remove:hover {
color: #dc3545;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>MES 數據表查詢工具</h1>
<p>點擊表名載入欄位 | 輸入篩選條件後查詢 | 套用篩選後取最後 1000 筆</p>
</div>
<div class="content">
{% for category, tables in tables_config.items() %}
<div class="table-category">
<div class="category-title">{{ category }}</div>
<div class="table-grid">
{% for table in tables %}
<div class="table-card" onclick="loadTableData('{{ table.name }}', '{{ table.display_name }}', '{{ table.time_field or '' }}')">
<div class="table-name">
{{ table.display_name }}
{% if table.row_count > 10000000 %}
<span class="badge large">大表</span>
{% endif %}
</div>
<div class="table-info">📦 數據量: {{ "{:,}".format(table.row_count) }} 行</div>
{% if table.time_field %}
<div class="table-info">🕐 時間欄位: {{ table.time_field }}</div>
{% endif %}
<div class="table-desc">{{ table.description }}</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
<div id="dataViewer" class="data-viewer">
<div class="viewer-header">
<h3 id="viewerTitle">數據查看器</h3>
<button class="close-btn" onclick="closeViewer()">關閉</button>
</div>
<div id="statsContainer"></div>
<div class="table-container">
<div id="tableContent"></div>
</div>
</div>
</div>
</div>
<script>
let currentTable = null;
let currentDisplayName = null;
let currentTimeField = null;
let currentColumns = [];
let currentFilters = {};
function loadTableData(tableName, displayName, timeField) {
// 標記當前選中的表
document.querySelectorAll('.table-card').forEach(card => {
card.classList.remove('active');
});
event.currentTarget.classList.add('active');
currentTable = tableName;
currentDisplayName = displayName;
currentTimeField = timeField || null;
currentFilters = {};
const viewer = document.getElementById('dataViewer');
const title = document.getElementById('viewerTitle');
const content = document.getElementById('tableContent');
const statsContainer = document.getElementById('statsContainer');
// 顯示查看器
viewer.classList.add('active');
title.textContent = `正在載入: ${displayName}`;
content.innerHTML = '<div class="loading">正在載入欄位資訊...</div>';
statsContainer.innerHTML = '';
// 滾動到查看器
viewer.scrollIntoView({ behavior: 'smooth', block: 'start' });
// 先取得欄位資訊
fetch('/api/get_table_columns', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ table_name: tableName })
})
.then(response => response.json())
.then(data => {
if (data.error) {
content.innerHTML = `<div class="error">${data.error}</div>`;
return;
}
currentColumns = data.columns;
title.textContent = `${displayName} (${currentColumns.length} 欄位)`;
// 顯示篩選控制區
renderFilterControls();
})
.catch(error => {
content.innerHTML = `<div class="error">請求失敗: ${error.message}</div>`;
});
}
function renderFilterControls() {
const statsContainer = document.getElementById('statsContainer');
const content = document.getElementById('tableContent');
// 統計區 + 查詢按鈕
statsContainer.innerHTML = `
<div class="stats">
<div class="stat-item">
<div class="stat-label">表名</div>
<div class="stat-value" style="font-size: 14px;">${currentTable}</div>
</div>
<div class="stat-item">
<div class="stat-label">欄位數</div>
<div class="stat-value">${currentColumns.length}</div>
</div>
<span class="filter-hint">在下方輸入框填入篩選條件 (模糊匹配)</span>
<button class="query-btn" onclick="executeQuery()">查詢</button>
<button class="clear-btn" onclick="clearFilters()">清除篩選</button>
</div>
<div id="activeFilters" class="active-filters"></div>
`;
// 生成表格結構 (含篩選輸入行)
let html = '<table><thead>';
// 欄位名稱行
html += '<tr>';
currentColumns.forEach(col => {
html += `<th>${col}</th>`;
});
html += '</tr>';
// 篩選輸入行
html += '<tr class="filter-row">';
currentColumns.forEach(col => {
html += `<th><input type="text" id="filter_${col}" placeholder="篩選..." onkeypress="handleFilterKeypress(event)" onchange="updateFilter('${col}', this.value)"></th>`;
});
html += '</tr>';
html += '</thead><tbody id="dataBody">';
html += '<tr><td colspan="' + currentColumns.length + '" style="text-align: center; padding: 40px; color: #666;">請輸入篩選條件後點擊「查詢」,或直接點擊「查詢」載入最後 1000 筆資料</td></tr>';
html += '</tbody></table>';
content.innerHTML = html;
}
function updateFilter(column, value) {
if (value && value.trim()) {
currentFilters[column] = value.trim();
} else {
delete currentFilters[column];
}
renderActiveFilters();
}
function renderActiveFilters() {
const container = document.getElementById('activeFilters');
if (!container) return;
const filterKeys = Object.keys(currentFilters);
if (filterKeys.length === 0) {
container.innerHTML = '';
return;
}
let html = '';
filterKeys.forEach(col => {
html += `<span class="filter-tag">${col}: ${currentFilters[col]} <span class="remove" onclick="removeFilter('${col}')">&times;</span></span>`;
});
container.innerHTML = html;
}
function removeFilter(column) {
delete currentFilters[column];
const input = document.getElementById(`filter_${column}`);
if (input) input.value = '';
renderActiveFilters();
}
function clearFilters() {
currentFilters = {};
currentColumns.forEach(col => {
const input = document.getElementById(`filter_${col}`);
if (input) input.value = '';
});
renderActiveFilters();
}
function handleFilterKeypress(event) {
if (event.key === 'Enter') {
executeQuery();
}
}
function executeQuery() {
const title = document.getElementById('viewerTitle');
const tbody = document.getElementById('dataBody');
// 收集所有篩選條件
currentFilters = {};
currentColumns.forEach(col => {
const input = document.getElementById(`filter_${col}`);
if (input && input.value.trim()) {
currentFilters[col] = input.value.trim();
}
});
renderActiveFilters();
title.textContent = `正在查詢: ${currentDisplayName}`;
tbody.innerHTML = `<tr><td colspan="${currentColumns.length}" class="loading">正在查詢資料...</td></tr>`;
// 發送查詢請求
fetch('/api/query_table', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
table_name: currentTable,
limit: 1000,
time_field: currentTimeField,
filters: Object.keys(currentFilters).length > 0 ? currentFilters : null
})
})
.then(response => response.json())
.then(data => {
if (data.error) {
tbody.innerHTML = `<tr><td colspan="${currentColumns.length}" class="error">${data.error}</td></tr>`;
return;
}
// 更新標題
const filterCount = Object.keys(currentFilters).length;
const filterText = filterCount > 0 ? ` [${filterCount} 個篩選]` : '';
title.textContent = `${currentDisplayName} (${data.row_count} 筆)${filterText}`;
// 生成資料行
if (data.data.length === 0) {
tbody.innerHTML = `<tr><td colspan="${currentColumns.length}" style="text-align: center; padding: 40px; color: #999;">查無資料</td></tr>`;
return;
}
let html = '';
data.data.forEach(row => {
html += '<tr>';
currentColumns.forEach(col => {
const value = row[col];
const displayValue = value === null ? '<i style="color: #999;">NULL</i>' : value;
html += `<td>${displayValue}</td>`;
});
html += '</tr>';
});
tbody.innerHTML = html;
})
.catch(error => {
tbody.innerHTML = `<tr><td colspan="${currentColumns.length}" class="error">請求失敗: ${error.message}</td></tr>`;
});
}
function closeViewer() {
document.getElementById('dataViewer').classList.remove('active');
document.querySelectorAll('.table-card').forEach(card => {
card.classList.remove('active');
});
currentTable = null;
currentColumns = [];
currentFilters = {};
}
</script>
</body>
</html>

140
apps/templates/portal.html Normal file
View File

@@ -0,0 +1,140 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MES 報表入口</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft JhengHei', Arial, sans-serif;
background: #f5f7fa;
color: #222;
}
.shell {
max-width: 1600px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 24px;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
margin-bottom: 16px;
}
.header h1 {
font-size: 26px;
margin-bottom: 6px;
}
.header p {
font-size: 14px;
opacity: 0.9;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 12px;
}
.tab {
border: 2px solid #d9d9d9;
background: white;
color: #333;
padding: 10px 18px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
}
.tab:hover {
border-color: #667eea;
color: #667eea;
}
.tab.active {
background: #667eea;
border-color: #667eea;
color: white;
}
.panel {
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
overflow: hidden;
}
.panel iframe {
width: 100%;
border: none;
display: none;
}
.panel iframe.active {
display: block;
}
</style>
</head>
<body>
<div class="shell">
<div class="header">
<h1>MES 報表入口</h1>
<p>統一入口WIP 報表、機台狀態報表與數據表查詢工具</p>
</div>
<div class="tabs">
<button class="tab active" data-target="wipFrame">WIP 在制品報表</button>
<button class="tab" data-target="resourceFrame">機台狀態報表</button>
<button class="tab" data-target="tableFrame">數據表查詢工具</button>
</div>
<div class="panel">
<iframe id="wipFrame" class="active" src="/wip" title="WIP 在制品報表"></iframe>
<iframe id="resourceFrame" src="/resource" title="機台狀態報表"></iframe>
<iframe id="tableFrame" src="/tables" title="數據表查詢工具"></iframe>
</div>
</div>
<script>
const tabs = document.querySelectorAll('.tab');
const frames = document.querySelectorAll('iframe');
function setFrameHeight() {
const headerHeight = document.querySelector('.header').offsetHeight;
const tabsHeight = document.querySelector('.tabs').offsetHeight;
const padding = 60;
const height = Math.max(600, window.innerHeight - headerHeight - tabsHeight - padding);
frames.forEach(frame => {
frame.style.height = `${height}px`;
});
}
function activateTab(targetId) {
tabs.forEach(tab => tab.classList.remove('active'));
frames.forEach(frame => frame.classList.remove('active'));
document.querySelector(`[data-target="${targetId}"]`).classList.add('active');
document.getElementById(targetId).classList.add('active');
}
tabs.forEach(tab => {
tab.addEventListener('click', () => activateTab(tab.dataset.target));
});
window.addEventListener('resize', setFrameHeight);
setFrameHeight();
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,940 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WIP 在制品報表</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft JhengHei', Arial, sans-serif;
background: #f5f7fa;
padding: 20px;
}
.container {
max-width: 1800px;
margin: 0 auto;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 30px;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.header h1 {
font-size: 32px;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
font-size: 16px;
}
.controls {
background: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.controls label {
font-weight: 600;
color: #333;
}
.controls select {
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: border-color 0.3s;
}
.controls select:hover {
border-color: #667eea;
}
.controls button {
background: #667eea;
color: white;
border: none;
padding: 10px 25px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background 0.3s;
}
.controls button:hover {
background: #5568d3;
}
.summary-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.card {
background: white;
padding: 25px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
transition: transform 0.3s, box-shadow 0.3s;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
}
.card-title {
font-size: 14px;
color: #666;
margin-bottom: 10px;
}
.card-value {
font-size: 28px;
font-weight: bold;
color: #667eea;
}
.charts-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.chart-container {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.chart-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
}
.chart {
width: 100%;
height: 350px;
}
.chart-large {
height: 450px;
}
.section {
background: white;
padding: 25px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.section-title {
font-size: 20px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
thead {
background: #f8f9fa;
}
th {
padding: 12px;
text-align: left;
border-bottom: 2px solid #dee2e6;
font-weight: 600;
color: #495057;
position: sticky;
top: 0;
background: #f8f9fa;
}
td {
padding: 12px;
border-bottom: 1px solid #f0f0f0;
}
tbody tr:hover {
background: #f8f9fa;
}
.number {
text-align: right;
font-family: 'Courier New', monospace;
}
.loading {
text-align: center;
padding: 40px;
color: #667eea;
font-size: 16px;
}
.error {
background: #fee;
color: #c33;
padding: 20px;
border-radius: 6px;
margin: 20px 0;
}
.tab-buttons {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.tab-button {
padding: 12px 24px;
background: white;
border: 2px solid #ddd;
border-radius: 6px;
cursor: pointer;
font-size: 15px;
transition: all 0.3s;
}
.tab-button:hover {
border-color: #667eea;
color: #667eea;
}
.tab-button.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.table-container {
overflow-x: auto;
max-height: 500px;
overflow-y: auto;
}
.metric-toggle {
display: flex;
gap: 10px;
margin-left: auto;
}
.metric-btn {
padding: 6px 12px;
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.metric-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>WIP 在制品報表</h1>
<p>查詢當前在制品 (Work In Process) 的數量統計與分析</p>
</div>
<div class="controls">
<button onclick="loadData()" style="background: #28a745;">重新整理</button>
<div class="metric-toggle">
<label>指標:</label>
<button class="metric-btn active" onclick="setMetric('qty')" id="btnQty">數量(QTY)</button>
<button class="metric-btn" onclick="setMetric('lot')" id="btnLot">LOT數</button>
<button class="metric-btn" onclick="setMetric('qty2')" id="btnQty2">片數(QTY2)</button>
</div>
<span id="lastUpdate" style="margin-left: auto; color: #666;"></span>
</div>
<!-- 總覽卡片 -->
<div class="summary-cards">
<div class="card">
<div class="card-title">總 LOT 數</div>
<div class="card-value" id="totalLotCount">-</div>
</div>
<div class="card">
<div class="card-title">總數量 (QTY)</div>
<div class="card-value" id="totalQty">-</div>
</div>
<div class="card">
<div class="card-title">總片數 (QTY2)</div>
<div class="card-value" id="totalQty2">-</div>
</div>
<div class="card">
<div class="card-title">工序數 (SPEC)</div>
<div class="card-value" id="specCount">-</div>
</div>
<div class="card">
<div class="card-title">工作中心數</div>
<div class="card-value" id="workcenterCount">-</div>
</div>
<div class="card">
<div class="card-title">產品線數</div>
<div class="card-value" id="productLineCount">-</div>
</div>
</div>
<!-- 圖表區域 - 第一排 -->
<div class="charts-row">
<div class="chart-container">
<div class="chart-title">產品線 WIP 分布</div>
<div id="chartProductLine" class="chart"></div>
</div>
<div class="chart-container">
<div class="chart-title">狀態分布</div>
<div id="chartStatus" class="chart"></div>
</div>
</div>
<!-- 圖表區域 - 第二排 -->
<div class="charts-row">
<div class="chart-container">
<div class="chart-title">Top 15 工單 (GA)</div>
<div id="chartMfgOrder" class="chart"></div>
</div>
<div class="chart-container">
<div class="chart-title">Top 15 工作中心</div>
<div id="chartWorkcenter" class="chart"></div>
</div>
</div>
<!-- Tab 切換 -->
<div class="tab-buttons">
<button class="tab-button active" onclick="switchTab('spec')">按工序與工作中心</button>
<button class="tab-button" onclick="switchTab('product')">按產品線明細</button>
<button class="tab-button" onclick="switchTab('mfgorder')">按工單明細</button>
</div>
<!-- Tab 1: 工序與工作中心 -->
<div id="specTab" class="tab-content active">
<div class="section">
<div class="section-title">各 SPEC 及 WORKCENTER 對應的 WIP 數量</div>
<div class="table-container">
<table id="specTable">
<thead>
<tr>
<th>SPECNAME (工序)</th>
<th>WORKCENTERNAME (工作中心)</th>
<th class="number">LOT 數</th>
<th class="number">總數量 (QTY)</th>
<th class="number">總片數 (QTY2)</th>
</tr>
</thead>
<tbody id="specTableBody">
<tr><td colspan="5" class="loading">正在載入數據...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Tab 2: 產品線明細 -->
<div id="productTab" class="tab-content">
<div class="section">
<div class="section-title">產品線匯總</div>
<div class="table-container">
<table id="productSummaryTable">
<thead>
<tr>
<th>PRODUCTLINENAME_LEF (產品線)</th>
<th class="number">LOT 數</th>
<th class="number">總數量 (QTY)</th>
<th class="number">總片數 (QTY2)</th>
</tr>
</thead>
<tbody id="productSummaryTableBody">
<tr><td colspan="4" class="loading">正在載入數據...</td></tr>
</tbody>
</table>
</div>
</div>
<div class="section">
<div class="section-title">產品線明細 (依工序與工作中心展開)</div>
<div class="table-container">
<table id="productDetailTable">
<thead>
<tr>
<th>PRODUCTLINENAME_LEF</th>
<th>SPECNAME</th>
<th>WORKCENTERNAME</th>
<th class="number">LOT 數</th>
<th class="number">總數量</th>
<th class="number">總片數</th>
</tr>
</thead>
<tbody id="productDetailTableBody">
<tr><td colspan="6" class="loading">正在載入數據...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Tab 3: 工單明細 -->
<div id="mfgorderTab" class="tab-content">
<div class="section">
<div class="section-title">各工單 (GA) WIP 統計</div>
<div class="table-container">
<table id="mfgorderTable">
<thead>
<tr>
<th>MFGORDERNAME (GA 工單)</th>
<th class="number">LOT 數</th>
<th class="number">總數量 (QTY)</th>
<th class="number">總片數 (QTY2)</th>
</tr>
</thead>
<tbody id="mfgorderTableBody">
<tr><td colspan="4" class="loading">正在載入數據...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
let currentTab = 'spec';
let currentMetric = 'qty'; // qty, lot, qty2
// 圖表實例
let chartProductLine, chartStatus, chartMfgOrder, chartWorkcenter;
// 緩存數據
let cachedData = {
productLine: null,
status: null,
mfgOrder: null,
specWorkcenter: null
};
// 初始化圖表
function initCharts() {
chartProductLine = echarts.init(document.getElementById('chartProductLine'));
chartStatus = echarts.init(document.getElementById('chartStatus'));
chartMfgOrder = echarts.init(document.getElementById('chartMfgOrder'));
chartWorkcenter = echarts.init(document.getElementById('chartWorkcenter'));
// 響應式
window.addEventListener('resize', () => {
chartProductLine.resize();
chartStatus.resize();
chartMfgOrder.resize();
chartWorkcenter.resize();
});
}
function switchTab(tab) {
document.querySelectorAll('.tab-button').forEach((btn, idx) => {
btn.classList.remove('active');
if ((tab === 'spec' && idx === 0) ||
(tab === 'product' && idx === 1) ||
(tab === 'mfgorder' && idx === 2)) {
btn.classList.add('active');
}
});
document.getElementById('specTab').classList.remove('active');
document.getElementById('productTab').classList.remove('active');
document.getElementById('mfgorderTab').classList.remove('active');
document.getElementById(tab + 'Tab').classList.add('active');
currentTab = tab;
}
function setMetric(metric) {
currentMetric = metric;
document.querySelectorAll('.metric-btn').forEach(btn => btn.classList.remove('active'));
document.getElementById('btn' + metric.charAt(0).toUpperCase() + metric.slice(1)).classList.add('active');
updateCharts();
}
function formatNumber(num) {
if (num === null || num === undefined) return '-';
return num.toLocaleString('zh-TW');
}
function getMetricValue(row) {
switch(currentMetric) {
case 'lot': return row.LOT_COUNT || 0;
case 'qty2': return row.TOTAL_QTY2 || 0;
default: return row.TOTAL_QTY || 0;
}
}
function getMetricLabel() {
switch(currentMetric) {
case 'lot': return 'LOT 數';
case 'qty2': return '片數';
default: return '數量';
}
}
// 更新所有圖表
function updateCharts() {
if (cachedData.productLine) updateProductLineChart(cachedData.productLine);
if (cachedData.status) updateStatusChart(cachedData.status);
if (cachedData.mfgOrder) updateMfgOrderChart(cachedData.mfgOrder);
if (cachedData.specWorkcenter) updateWorkcenterChart(cachedData.specWorkcenter);
}
// 產品線長條圖
function updateProductLineChart(data) {
const sorted = [...data].sort((a, b) => getMetricValue(b) - getMetricValue(a)).slice(0, 10);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value',
axisLabel: { formatter: val => val >= 1000 ? (val/1000).toFixed(0) + 'K' : val }
},
yAxis: {
type: 'category',
data: sorted.map(d => d.PRODUCTLINENAME_LEF || '(空)').reverse(),
axisLabel: {
width: 120,
overflow: 'truncate'
}
},
series: [{
name: getMetricLabel(),
type: 'bar',
data: sorted.map(d => getMetricValue(d)).reverse(),
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#667eea' },
{ offset: 1, color: '#764ba2' }
])
},
label: {
show: true,
position: 'right',
formatter: params => formatNumber(params.value)
}
}]
};
chartProductLine.setOption(option);
}
// 狀態圓餅圖
function updateStatusChart(data) {
const statusColors = {
'Queue': '#36a2eb',
'Run': '#4bc0c0',
'Hold': '#ff6384',
'Complete': '#9966ff',
'Scrapped': '#999999'
};
const pieData = data.map(d => ({
name: d.STATUS_NAME,
value: getMetricValue(d)
}));
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center'
},
series: [{
name: getMetricLabel(),
type: 'pie',
radius: ['40%', '70%'],
center: ['40%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 20,
fontWeight: 'bold'
}
},
labelLine: { show: false },
data: pieData.map(d => ({
...d,
itemStyle: { color: statusColors[d.name] || '#667eea' }
}))
}]
};
chartStatus.setOption(option);
}
// 工單長條圖
function updateMfgOrderChart(data) {
const sorted = [...data].sort((a, b) => getMetricValue(b) - getMetricValue(a)).slice(0, 15);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value',
axisLabel: { formatter: val => val >= 1000 ? (val/1000).toFixed(0) + 'K' : val }
},
yAxis: {
type: 'category',
data: sorted.map(d => d.MFGORDERNAME || '(空)').reverse(),
axisLabel: {
width: 100,
overflow: 'truncate'
}
},
series: [{
name: getMetricLabel(),
type: 'bar',
data: sorted.map(d => getMetricValue(d)).reverse(),
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#36a2eb' },
{ offset: 1, color: '#4bc0c0' }
])
},
label: {
show: true,
position: 'right',
formatter: params => formatNumber(params.value)
}
}]
};
chartMfgOrder.setOption(option);
}
// 工作中心長條圖
function updateWorkcenterChart(data) {
// 按工作中心匯總
const wcMap = {};
data.forEach(row => {
const wc = row.WORKCENTERNAME || '(空)';
if (!wcMap[wc]) {
wcMap[wc] = { LOT_COUNT: 0, TOTAL_QTY: 0, TOTAL_QTY2: 0 };
}
wcMap[wc].LOT_COUNT += row.LOT_COUNT || 0;
wcMap[wc].TOTAL_QTY += row.TOTAL_QTY || 0;
wcMap[wc].TOTAL_QTY2 += row.TOTAL_QTY2 || 0;
});
const wcData = Object.entries(wcMap).map(([name, values]) => ({
WORKCENTERNAME: name,
...values
}));
const sorted = wcData.sort((a, b) => getMetricValue(b) - getMetricValue(a)).slice(0, 15);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value',
axisLabel: { formatter: val => val >= 1000 ? (val/1000).toFixed(0) + 'K' : val }
},
yAxis: {
type: 'category',
data: sorted.map(d => d.WORKCENTERNAME).reverse(),
axisLabel: {
width: 100,
overflow: 'truncate'
}
},
series: [{
name: getMetricLabel(),
type: 'bar',
data: sorted.map(d => getMetricValue(d)).reverse(),
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#ff6384' },
{ offset: 1, color: '#ff9f40' }
])
},
label: {
show: true,
position: 'right',
formatter: params => formatNumber(params.value)
}
}]
};
chartWorkcenter.setOption(option);
}
async function loadSummary() {
try {
const response = await fetch('/api/wip/summary');
const result = await response.json();
if (result.success) {
const data = result.data;
document.getElementById('totalLotCount').textContent = formatNumber(data.total_lot_count);
document.getElementById('totalQty').textContent = formatNumber(data.total_qty);
document.getElementById('totalQty2').textContent = formatNumber(data.total_qty2);
document.getElementById('specCount').textContent = formatNumber(data.spec_count);
document.getElementById('workcenterCount').textContent = formatNumber(data.workcenter_count);
document.getElementById('productLineCount').textContent = formatNumber(data.product_line_count);
}
} catch (error) {
console.error('Summary 請求失敗:', error);
}
}
async function loadSpecWorkcenterData() {
const tbody = document.getElementById('specTableBody');
tbody.innerHTML = '<tr><td colspan="5" class="loading">正在載入數據...</td></tr>';
try {
const response = await fetch('/api/wip/by_spec_workcenter');
const result = await response.json();
if (result.success) {
const data = result.data;
cachedData.specWorkcenter = data;
updateWorkcenterChart(data);
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center; color:#999;">無數據</td></tr>';
return;
}
let html = '';
data.forEach(row => {
html += `
<tr>
<td>${row.SPECNAME || '-'}</td>
<td>${row.WORKCENTERNAME || '-'}</td>
<td class="number">${formatNumber(row.LOT_COUNT)}</td>
<td class="number">${formatNumber(row.TOTAL_QTY)}</td>
<td class="number">${formatNumber(row.TOTAL_QTY2)}</td>
</tr>
`;
});
tbody.innerHTML = html;
} else {
tbody.innerHTML = `<tr><td colspan="5" class="error">${result.error}</td></tr>`;
}
} catch (error) {
tbody.innerHTML = `<tr><td colspan="5" class="error">請求失敗: ${error.message}</td></tr>`;
}
}
async function loadProductLineData() {
const summaryTbody = document.getElementById('productSummaryTableBody');
const detailTbody = document.getElementById('productDetailTableBody');
summaryTbody.innerHTML = '<tr><td colspan="4" class="loading">正在載入數據...</td></tr>';
detailTbody.innerHTML = '<tr><td colspan="6" class="loading">正在載入數據...</td></tr>';
try {
const response = await fetch('/api/wip/by_product_line');
const result = await response.json();
if (result.success) {
// 緩存並更新圖表
cachedData.productLine = result.summary;
updateProductLineChart(result.summary);
// 產品線匯總表
const summary = result.summary;
if (summary.length === 0) {
summaryTbody.innerHTML = '<tr><td colspan="4" style="text-align:center; color:#999;">無數據</td></tr>';
} else {
let summaryHtml = '';
summary.forEach(row => {
summaryHtml += `
<tr>
<td><strong>${row.PRODUCTLINENAME_LEF || '-'}</strong></td>
<td class="number"><strong>${formatNumber(row.LOT_COUNT)}</strong></td>
<td class="number"><strong>${formatNumber(row.TOTAL_QTY)}</strong></td>
<td class="number"><strong>${formatNumber(row.TOTAL_QTY2)}</strong></td>
</tr>
`;
});
summaryTbody.innerHTML = summaryHtml;
}
// 產品線明細
const data = result.data;
if (data.length === 0) {
detailTbody.innerHTML = '<tr><td colspan="6" style="text-align:center; color:#999;">無數據</td></tr>';
} else {
let detailHtml = '';
data.forEach(row => {
detailHtml += `
<tr>
<td>${row.PRODUCTLINENAME_LEF || '-'}</td>
<td>${row.SPECNAME || '-'}</td>
<td>${row.WORKCENTERNAME || '-'}</td>
<td class="number">${formatNumber(row.LOT_COUNT)}</td>
<td class="number">${formatNumber(row.TOTAL_QTY)}</td>
<td class="number">${formatNumber(row.TOTAL_QTY2)}</td>
</tr>
`;
});
detailTbody.innerHTML = detailHtml;
}
} else {
summaryTbody.innerHTML = `<tr><td colspan="4" class="error">${result.error}</td></tr>`;
detailTbody.innerHTML = `<tr><td colspan="6" class="error">${result.error}</td></tr>`;
}
} catch (error) {
summaryTbody.innerHTML = `<tr><td colspan="4" class="error">請求失敗: ${error.message}</td></tr>`;
detailTbody.innerHTML = `<tr><td colspan="6" class="error">請求失敗: ${error.message}</td></tr>`;
}
}
async function loadStatusData() {
try {
const response = await fetch('/api/wip/by_status');
const result = await response.json();
if (result.success) {
cachedData.status = result.data;
updateStatusChart(result.data);
}
} catch (error) {
console.error('Status 請求失敗:', error);
}
}
async function loadMfgOrderData() {
const tbody = document.getElementById('mfgorderTableBody');
tbody.innerHTML = '<tr><td colspan="4" class="loading">正在載入數據...</td></tr>';
try {
const response = await fetch('/api/wip/by_mfgorder?limit=100');
const result = await response.json();
if (result.success) {
cachedData.mfgOrder = result.data;
updateMfgOrderChart(result.data);
const data = result.data;
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; color:#999;">無數據</td></tr>';
return;
}
let html = '';
data.forEach(row => {
html += `
<tr>
<td>${row.MFGORDERNAME || '-'}</td>
<td class="number">${formatNumber(row.LOT_COUNT)}</td>
<td class="number">${formatNumber(row.TOTAL_QTY)}</td>
<td class="number">${formatNumber(row.TOTAL_QTY2)}</td>
</tr>
`;
});
tbody.innerHTML = html;
} else {
tbody.innerHTML = `<tr><td colspan="4" class="error">${result.error}</td></tr>`;
}
} catch (error) {
tbody.innerHTML = `<tr><td colspan="4" class="error">請求失敗: ${error.message}</td></tr>`;
}
}
function loadData() {
const now = new Date();
document.getElementById('lastUpdate').textContent = `最後更新: ${now.toLocaleString('zh-TW')}`;
loadSummary();
loadSpecWorkcenterData();
loadProductLineData();
loadStatusData();
loadMfgOrderData();
}
// 頁面載入時初始化
window.onload = function() {
initCharts();
loadData();
};
</script>
</body>
</html>

401
apps/wip_report.py Normal file
View File

@@ -0,0 +1,401 @@
"""
WIP 報表查詢工具
查詢當前在制品 (Work In Process) 的數量統計
"""
import oracledb
import pandas as pd
from flask import Flask, render_template, request, jsonify
from datetime import datetime, timedelta
# 數據庫連接配置
DB_CONFIG = {
'user': 'MBU1_R',
'password': 'Pj2481mbu1',
'dsn': '10.1.1.58:1521/DWDB'
}
app = Flask(__name__)
def get_db_connection():
"""建立數據庫連接"""
try:
connection = oracledb.connect(**DB_CONFIG)
return connection
except Exception as e:
print(f"數據庫連接失敗: {e}")
return None
def query_wip_by_spec_workcenter(days=7):
"""
查詢各 SPECNAME 及 WORKCENTERNAME 對應的當下 WIP 數量
Args:
days: 查詢最近幾天的數據(默認 7 天)
Returns:
DataFrame: 包含統計數據
"""
connection = get_db_connection()
if not connection:
return None
try:
# SQL 查詢:按 SPECNAME 和 WORKCENTERNAME 統計
sql = """
SELECT
SPECNAME,
WORKCENTERNAME,
COUNT(DISTINCT CONTAINERNAME) as LOT_COUNT,
SUM(QTY) as TOTAL_QTY,
SUM(QTY2) as TOTAL_QTY2
FROM DW_MES_WIP
WHERE TXNDATE >= TRUNC(SYSDATE) - :days
AND STATUS NOT IN (8, 128) -- 排除已完成/取消
AND SPECNAME IS NOT NULL
AND WORKCENTERNAME IS NOT NULL
GROUP BY SPECNAME, WORKCENTERNAME
ORDER BY SPECNAME, WORKCENTERNAME
"""
df = pd.read_sql(sql, connection, params={'days': days})
connection.close()
return df
except Exception as e:
if connection:
connection.close()
print(f"查詢失敗: {e}")
return None
def query_wip_by_product_line(days=7):
"""
查詢不同產品線的 WIP 數量分布
Args:
days: 查詢最近幾天的數據(默認 7 天)
Returns:
DataFrame: 包含產品線統計數據
"""
connection = get_db_connection()
if not connection:
return None
try:
# SQL 查詢:按產品線統計
sql = """
SELECT
PRODUCTLINENAME_LEF,
SPECNAME,
WORKCENTERNAME,
COUNT(DISTINCT CONTAINERNAME) as LOT_COUNT,
SUM(QTY) as TOTAL_QTY,
SUM(QTY2) as TOTAL_QTY2
FROM DW_MES_WIP
WHERE TXNDATE >= TRUNC(SYSDATE) - :days
AND STATUS NOT IN (8, 128) -- 排除已完成/取消
AND PRODUCTLINENAME_LEF IS NOT NULL
GROUP BY PRODUCTLINENAME_LEF, SPECNAME, WORKCENTERNAME
ORDER BY PRODUCTLINENAME_LEF, SPECNAME, WORKCENTERNAME
"""
df = pd.read_sql(sql, connection, params={'days': days})
connection.close()
return df
except Exception as e:
if connection:
connection.close()
print(f"查詢失敗: {e}")
return None
def query_wip_summary(days=7):
"""
查詢 WIP 總覽統計
Returns:
dict: 包含總體統計數據
"""
connection = get_db_connection()
if not connection:
return None
try:
# SQL 查詢:總覽統計
sql = """
SELECT
COUNT(DISTINCT CONTAINERNAME) as TOTAL_LOT_COUNT,
SUM(QTY) as TOTAL_QTY,
SUM(QTY2) as TOTAL_QTY2,
COUNT(DISTINCT SPECNAME) as SPEC_COUNT,
COUNT(DISTINCT WORKCENTERNAME) as WORKCENTER_COUNT,
COUNT(DISTINCT PRODUCTLINENAME_LEF) as PRODUCT_LINE_COUNT
FROM DW_MES_WIP
WHERE TXNDATE >= TRUNC(SYSDATE) - :days
AND STATUS NOT IN (8, 128)
"""
cursor = connection.cursor()
cursor.execute(sql, {'days': days})
result = cursor.fetchone()
cursor.close()
connection.close()
if result:
return {
'total_lot_count': result[0] or 0,
'total_qty': result[1] or 0,
'total_qty2': result[2] or 0,
'spec_count': result[3] or 0,
'workcenter_count': result[4] or 0,
'product_line_count': result[5] or 0
}
return None
except Exception as e:
if connection:
connection.close()
print(f"查詢失敗: {e}")
return None
@app.route('/')
def index():
"""首頁"""
return render_template('wip_report.html')
@app.route('/api/wip/summary')
def api_wip_summary():
"""API: WIP 總覽統計"""
days = request.args.get('days', 7, type=int)
summary = query_wip_summary(days)
if summary:
return jsonify({'success': True, 'data': summary})
else:
return jsonify({'success': False, 'error': '查詢失敗'}), 500
@app.route('/api/wip/by_spec_workcenter')
def api_wip_by_spec_workcenter():
"""API: 按 SPEC 和 WORKCENTER 統計"""
days = request.args.get('days', 7, type=int)
df = query_wip_by_spec_workcenter(days)
if df is not None:
# 轉換為 JSON
data = df.to_dict(orient='records')
return jsonify({'success': True, 'data': data, 'count': len(data)})
else:
return jsonify({'success': False, 'error': '查詢失敗'}), 500
@app.route('/api/wip/by_product_line')
def api_wip_by_product_line():
"""API: 按產品線統計"""
days = request.args.get('days', 7, type=int)
df = query_wip_by_product_line(days)
if df is not None:
# 轉換為 JSON
data = df.to_dict(orient='records')
# 計算產品線匯總
if not df.empty:
product_line_summary = df.groupby('PRODUCTLINENAME_LEF').agg({
'LOT_COUNT': 'sum',
'TOTAL_QTY': 'sum',
'TOTAL_QTY2': 'sum'
}).reset_index()
summary = product_line_summary.to_dict(orient='records')
else:
summary = []
return jsonify({
'success': True,
'data': data,
'summary': summary,
'count': len(data)
})
else:
return jsonify({'success': False, 'error': '查詢失敗'}), 500
def query_wip_by_status(days=7):
"""查詢各狀態的 WIP 分布"""
connection = get_db_connection()
if not connection:
return None
try:
sql = """
SELECT
CASE STATUS
WHEN 1 THEN 'Queue'
WHEN 2 THEN 'Run'
WHEN 4 THEN 'Hold'
WHEN 8 THEN 'Complete'
WHEN 128 THEN 'Scrapped'
ELSE 'Other(' || STATUS || ')'
END as STATUS_NAME,
STATUS,
COUNT(DISTINCT CONTAINERNAME) as LOT_COUNT,
SUM(QTY) as TOTAL_QTY,
SUM(QTY2) as TOTAL_QTY2
FROM DW_MES_WIP
WHERE TXNDATE >= TRUNC(SYSDATE) - :days
GROUP BY STATUS
ORDER BY LOT_COUNT DESC
"""
df = pd.read_sql(sql, connection, params={'days': days})
connection.close()
return df
except Exception as e:
if connection:
connection.close()
print(f"查詢失敗: {e}")
return None
def query_wip_by_mfgorder(days=7, limit=20):
"""查詢各工單 (GA) 的 WIP 分布"""
connection = get_db_connection()
if not connection:
return None
try:
sql = """
SELECT * FROM (
SELECT
MFGORDERNAME,
COUNT(DISTINCT CONTAINERNAME) as LOT_COUNT,
SUM(QTY) as TOTAL_QTY,
SUM(QTY2) as TOTAL_QTY2
FROM DW_MES_WIP
WHERE TXNDATE >= TRUNC(SYSDATE) - :days
AND STATUS NOT IN (8, 128)
AND MFGORDERNAME IS NOT NULL
GROUP BY MFGORDERNAME
ORDER BY LOT_COUNT DESC
) WHERE ROWNUM <= :limit
"""
df = pd.read_sql(sql, connection, params={'days': days, 'limit': limit})
connection.close()
return df
except Exception as e:
if connection:
connection.close()
print(f"查詢失敗: {e}")
return None
def query_wip_heatmap(days=7):
"""查詢 SPEC × WORKCENTER 熱力圖數據"""
connection = get_db_connection()
if not connection:
return None
try:
sql = """
SELECT
SPECNAME,
WORKCENTERNAME,
SUM(QTY) as TOTAL_QTY
FROM DW_MES_WIP
WHERE TXNDATE >= TRUNC(SYSDATE) - :days
AND STATUS NOT IN (8, 128)
AND SPECNAME IS NOT NULL
AND WORKCENTERNAME IS NOT NULL
GROUP BY SPECNAME, WORKCENTERNAME
ORDER BY SPECNAME, WORKCENTERNAME
"""
df = pd.read_sql(sql, connection, params={'days': days})
connection.close()
return df
except Exception as e:
if connection:
connection.close()
print(f"查詢失敗: {e}")
return None
@app.route('/api/wip/by_status')
def api_wip_by_status():
"""API: 按狀態統計"""
days = request.args.get('days', 7, type=int)
df = query_wip_by_status(days)
if df is not None:
data = df.to_dict(orient='records')
return jsonify({'success': True, 'data': data})
else:
return jsonify({'success': False, 'error': '查詢失敗'}), 500
@app.route('/api/wip/by_mfgorder')
def api_wip_by_mfgorder():
"""API: 按工單統計 (Top 20)"""
days = request.args.get('days', 7, type=int)
limit = request.args.get('limit', 20, type=int)
df = query_wip_by_mfgorder(days, limit)
if df is not None:
data = df.to_dict(orient='records')
return jsonify({'success': True, 'data': data})
else:
return jsonify({'success': False, 'error': '查詢失敗'}), 500
@app.route('/api/wip/heatmap')
def api_wip_heatmap():
"""API: SPEC × WORKCENTER 熱力圖數據"""
days = request.args.get('days', 7, type=int)
df = query_wip_heatmap(days)
if df is not None:
if df.empty:
return jsonify({'success': True, 'specs': [], 'workcenters': [], 'data': []})
specs = sorted(df['SPECNAME'].unique().tolist())
workcenters = sorted(df['WORKCENTERNAME'].unique().tolist())
# 轉換為熱力圖格式 [workcenter_index, spec_index, value]
heatmap_data = []
for _, row in df.iterrows():
spec_idx = specs.index(row['SPECNAME'])
wc_idx = workcenters.index(row['WORKCENTERNAME'])
heatmap_data.append([wc_idx, spec_idx, int(row['TOTAL_QTY'] or 0)])
return jsonify({
'success': True,
'specs': specs,
'workcenters': workcenters,
'data': heatmap_data
})
else:
return jsonify({'success': False, 'error': '查詢失敗'}), 500
if __name__ == '__main__':
# 測試數據庫連接
print("正在測試數據庫連接...")
conn = get_db_connection()
if conn:
print("✓ 數據庫連接成功!")
conn.close()
# 測試查詢
print("\n正在測試查詢...")
summary = query_wip_summary()
if summary:
print(f"✓ WIP 總覽查詢成功!")
print(f" - 總 LOT 數: {summary['total_lot_count']}")
print(f" - 總數量: {summary['total_qty']}")
print(f" - 總片數: {summary['total_qty2']}")
print("\n啟動 Web 服務器...")
print("請訪問: http://localhost:5001")
print("按 Ctrl+C 停止服務器\n")
app.run(debug=True, host='0.0.0.0', port=5001)
else:
print("✗ 數據庫連接失敗,請檢查配置")

29
apps/快速啟動.py Normal file
View File

@@ -0,0 +1,29 @@
"""
快速啟動腳本 - 可以直接用 Python 運行
使用方法: python apps\快速啟動.py
"""
import sys
import os
# 檢查依賴
try:
import flask
import pandas
import oracledb
print("✓ 所有依賴已安裝")
except ImportError as e:
print(f"[錯誤] 缺少依賴: {e}")
print("\n請先執行以下命令安裝依賴:")
print(" pip install flask pandas oracledb")
print("\n或者運行: scripts\\0_初始化環境.bat")
sys.exit(1)
# 啟動應用
print("\n正在啟動 MES 報表入口...")
print("請訪問: http://localhost:5000")
print("按 Ctrl+C 停止服務器\n")
# 導入並運行
from portal import app
app.run(debug=True, host='0.0.0.0', port=5000)

10352
data/table_schema_info.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

282
docs/WIP報表說明.md Normal file
View File

@@ -0,0 +1,282 @@
# WIP 在制品報表 - 使用說明
## 功能說明
這是一個專門查詢當前在制品 (Work In Process) 數量統計的報表工具。
### 欄位對照
根據您的需求,系統會顯示以下欄位:
- **CONTAINERNAME** = LOT ID (批次號)
- **GA_CONTAINERNAME** = GA LOT ID (GA 批次號)
- **QTY** = 數量
- **QTY2** = 片數
- **MFGORDERNAME** = GA (工單號)
### 報表內容
#### 1. 總覽統計卡片
- 總 LOT 數:當前在制品的批次總數
- 總數量 (QTY):總數量統計
- 總片數 (QTY2):總片數統計
- 工序數 (SPEC):涉及的工序數量
- 工作中心數:涉及的工作中心數量
- 產品線數:涉及的產品線數量
#### 2. 按工序與工作中心統計
顯示各 SPECNAME (工序) 及 WORKCENTERNAME (工作中心) 對應的當前 WIP 數量:
- SPECNAME (工序)
- WORKCENTERNAME (工作中心)
- LOT 數
- 總數量 (QTY)
- 總片數 (QTY2)
#### 3. 按產品線統計
顯示不同產品組合 (PRODUCTLINENAME_LEF) 各佔的量:
**產品線匯總**
- PRODUCTLINENAME_LEF (產品線)
- LOT 數合計
- 總數量 (QTY) 合計
- 總片數 (QTY2) 合計
**產品線明細**
- PRODUCTLINENAME_LEF (產品線)
- SPECNAME (工序)
- WORKCENTERNAME (工作中心)
- LOT 數
- 總數量 (QTY)
- 總片數 (QTY2)
---
## 啟動方式
### 方法 1: 使用啟動腳本(推薦)
```bash
# 雙擊運行
scripts\啟動Dashboard.bat
```
### 方法 2: 手動啟動
```bash
# 使用虛擬環境的 Python
venv\Scripts\python.exe apps\portal.py
```
然後訪問: **http://localhost:5000**
(入口頁面可用 Tab 切換,或直接開啟 **http://localhost:5000/wip**
---
## 使用說明
### 1. 選擇時間範圍
在頁面頂部的下拉選單中選擇:
- 最近 1 天
- 最近 3 天
- 最近 7 天(默認)
- 最近 14 天
- 最近 30 天
### 2. 點擊查詢
選擇時間範圍後,點擊「🔍 查詢」按鈕重新載入數據
### 3. 切換報表視圖
使用頁面中的標籤切換不同的統計視圖:
- **按工序與工作中心統計**:查看各 SPEC 和 WORKCENTER 的 WIP 分布
- **按產品線統計**:查看各產品線的 WIP 分布(包含匯總和明細)
---
## 查詢邏輯
### 數據範圍
- 使用 `TXNDATE >= TRUNC(SYSDATE) - N` 查詢最近 N 天的數據
- 自動排除已完成或已取消的批次 (`STATUS NOT IN (8, 128)`)
- 只查詢有效的數據(非 NULL
### SQL 查詢示例
#### 按 SPEC 和 WORKCENTER 統計
```sql
SELECT
SPECNAME,
WORKCENTERNAME,
COUNT(DISTINCT CONTAINERNAME) as LOT_COUNT,
SUM(QTY) as TOTAL_QTY,
SUM(QTY2) as TOTAL_QTY2
FROM DW_MES_WIP
WHERE TXNDATE >= TRUNC(SYSDATE) - 7
AND STATUS NOT IN (8, 128)
AND SPECNAME IS NOT NULL
AND WORKCENTERNAME IS NOT NULL
GROUP BY SPECNAME, WORKCENTERNAME
ORDER BY SPECNAME, WORKCENTERNAME
```
#### 按產品線統計
```sql
SELECT
PRODUCTLINENAME_LEF,
SPECNAME,
WORKCENTERNAME,
COUNT(DISTINCT CONTAINERNAME) as LOT_COUNT,
SUM(QTY) as TOTAL_QTY,
SUM(QTY2) as TOTAL_QTY2
FROM DW_MES_WIP
WHERE TXNDATE >= TRUNC(SYSDATE) - 7
AND STATUS NOT IN (8, 128)
AND PRODUCTLINENAME_LEF IS NOT NULL
GROUP BY PRODUCTLINENAME_LEF, SPECNAME, WORKCENTERNAME
ORDER BY PRODUCTLINENAME_LEF, SPECNAME, WORKCENTERNAME
```
---
## API 接口
如需程式化調用,可使用以下 API
### 1. WIP 總覽統計
```
GET /api/wip/summary?days=7
```
**響應**:
```json
{
"success": true,
"data": {
"total_lot_count": 1234,
"total_qty": 567890,
"total_qty2": 123456,
"spec_count": 45,
"workcenter_count": 23,
"product_line_count": 12
}
}
```
### 2. 按 SPEC 和 WORKCENTER 統計
```
GET /api/wip/by_spec_workcenter?days=7
```
**響應**:
```json
{
"success": true,
"data": [
{
"SPECNAME": "SMT",
"WORKCENTERNAME": "SMT-LINE1",
"LOT_COUNT": 50,
"TOTAL_QTY": 12500,
"TOTAL_QTY2": 2500
}
],
"count": 100
}
```
### 3. 按產品線統計
```
GET /api/wip/by_product_line?days=7
```
**響應**:
```json
{
"success": true,
"data": [...],
"summary": [
{
"PRODUCTLINENAME_LEF": "產品線A",
"LOT_COUNT": 150,
"TOTAL_QTY": 37500,
"TOTAL_QTY2": 7500
}
],
"count": 200
}
```
---
## 注意事項
### 1. 性能考量
- 查詢使用 `TXNDATE` 欄位進行時間範圍過濾
- 建議查詢範圍不超過 30 天
- 大數據量查詢可能需要等待幾秒鐘
### 2. 數據即時性
- 數據來自 DW_MES_WIP 表
- 根據 `TXNDATE` 欄位判斷數據更新時間
- 如需最新數據,建議選擇「最近 1 天」或「最近 3 天」
### 3. 狀態過濾
系統自動排除以下狀態的批次:
- `STATUS = 8`: 已完成
- `STATUS = 128`: 已取消
---
## 常見問題
### Q1: 為什麼查詢結果為空?
**可能原因**
1. 選擇的時間範圍內沒有數據
2. 所有批次都已完成或取消
3. 數據庫連接問題
**解決方法**
1. 嘗試擴大時間範圍(例如選擇「最近 30 天」)
2. 檢查數據庫連接狀態
### Q2: 數字顯示為 "-" 是什麼意思?
表示該欄位的值為 NULL 或查詢失敗。
### Q3: 如何匯出數據?
目前版本不支援匯出功能,但您可以:
1. 直接使用 API 接口獲取 JSON 格式數據
2. 從瀏覽器複製表格內容到 Excel
### Q4: 可以同時運行多個報表嗎?
可以,但需要使用不同的端口:
- 數據查詢工具: http://localhost:5000
- WIP 報表: http://localhost:5000
---
## 後續擴展
可以考慮增加的功能:
- [ ] Excel 匯出功能
- [ ] 圖表可視化(餅圖、柱狀圖)
- [ ] 更多篩選條件(產品、工單號等)
- [ ] Hold 批次統計
- [ ] 在站時間分析
---
**版本**: 1.0
**建立日期**: 2026-01-14

210
docs/使用說明.md Normal file
View File

@@ -0,0 +1,210 @@
# MES 數據查詢工具 - 使用說明
## 工具用途
這是一個 Web 界面工具,用於快速查看 MES 數據庫中各表的實際資料,幫助您:
- ✅ 確認表結構和欄位內容
- ✅ 查看數據樣本(最後 1000 筆)
- ✅ 驗證時間欄位和數據格式
- ✅ 了解表的實際使用情況
---
## 啟動步驟
### 首次使用(需要初始化環境)
1. **雙擊運行**: `scripts\0_初始化環境.bat`
- 會自動創建 Python 虛擬環境
- 自動安裝所需依賴Flask, Pandas, oracledb
- 測試數據庫連接
2. **雙擊運行**: `scripts\啟動Dashboard.bat`
- 啟動 Web 服務器
3. **打開瀏覽器訪問**: http://localhost:5000
- 上方 Tab 可切換「WIP 報表 / 數據表查詢工具」
### 後續使用
直接雙擊運行 `scripts\啟動Dashboard.bat` 即可
---
## 使用界面說明
### 主界面
打開 http://localhost:5000 後您會看到入口頁面請切換到「數據表查詢工具」Tab
(或直接開啟 http://localhost:5000/tables
```
┌─────────────────────────────────────────────┐
│ 📊 MES 數據表查詢工具 │
│ 點擊表名查看最後 1000 筆資料 │
├─────────────────────────────────────────────┤
│ 現況快照表 │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ WIP (在制品) │ │ RESOURCE │ │
│ │ 77,470,834行 │ │ 90,620行 │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ 歷史累積表 │
│ ┌──────────────────┐ ┌──────────────┐ │
│ │ RESOURCESTATUS ⭐ │ │ LOTWIPHISTORY⭐│ │
│ │ 65,139,825行 │ │ 53,085,425行 │ │
│ └──────────────────┘ └──────────────┘ │
│ ... │
└─────────────────────────────────────────────┘
```
### 查看表資料
1. **點擊任一表卡片**
2. **系統會自動查詢並顯示**
- 數據統計(返回行數、欄位數)
- 完整的欄位列表(表頭)
- 最後 1000 筆資料(表格形式)
3. **大表自動優化**
- 標有 "大表" 標籤的表會自動按時間欄位倒序排列
- 確保查看到的是最新數據
### 表卡片說明
每個表卡片顯示:
- **表名**: 完整表名和中文說明
- **數據量**: 總行數(格式化顯示)
- **時間欄位**: 主要時間欄位名稱(如果有)
- **用途描述**: 表的業務用途
- **大表標籤**: 超過 1000 萬行會顯示紅色 "大表" 標籤
---
## 表分類說明
### 🟢 現況快照表4張
這些表存儲**當前狀態**,數據會被更新或覆蓋:
- **DW_MES_WIP** - 在制品當前狀態(⚠️ 雖名為現況表,但包含 7700 萬行歷史)
- **DW_MES_RESOURCE** - 設備資源主檔
- **DW_MES_CONTAINER** - 容器當前狀態
- **DW_MES_JOB** - 設備維修工單當前狀態
### 🟡 歷史累積表10張
這些表只**新增不修改**,記錄完整歷史:
- **DW_MES_RESOURCESTATUS** ⭐ - 設備狀態變更歷史(用於計算稼動率)
- **DW_MES_LOTWIPHISTORY** ⭐ - 批次完整流轉歷史(用於 Cycle Time
- **DW_MES_LOTWIPDATAHISTORY** - 批次數據採集歷史
- **DW_MES_HM_LOTMOVEOUT** - 批次移出事件
- **DW_MES_JOBTXNHISTORY** - 維修工單交易歷史
- **DW_MES_LOTREJECTHISTORY** - 批次拒絕歷史
- **DW_MES_LOTMATERIALSHISTORY** - 物料消耗歷史
- **DW_MES_HOLDRELEASEHISTORY** - Hold/Release 歷史
- **DW_MES_MAINTENANCE** - 設備維護歷史
- **DW_MES_RESOURCESTATUS_SHIFT** - 資源班次狀態
### 🟣 輔助表2張
- **DW_MES_PARTREQUESTORDER** - 物料請求訂單
- **DW_MES_PJ_COMBINEDASSYLOTS** - 組合裝配批次
---
## 使用技巧
### 1. 確認欄位內容
點擊表名後,觀察:
- **欄位名稱**: 表頭顯示所有欄位
- **數據類型**: 從數據內容推斷(數字、日期、文字)
- **NULL 值**: 顯示為灰色斜體 "NULL"
- **日期格式**: 自動格式化為 `YYYY-MM-DD HH:MM:SS`
### 2. 驗證時間欄位
對於大表,系統會自動按時間欄位排序,例如:
- **DW_MES_WIP**: 按 `TXNDATE` 倒序
- **DW_MES_RESOURCESTATUS**: 按 `OLDLASTSTATUSCHANGEDATE` 倒序
- **DW_MES_LOTWIPHISTORY**: 按 `TRACKINTIMESTAMP` 倒序
這樣可以快速看到最新的數據記錄。
### 3. 查看數據範圍
觀察返回的 1000 筆資料:
- 最新一筆的時間(第一行)
- 最舊一筆的時間(最後一行)
- 推算數據更新頻率
### 4. 確認表性質
通過查看資料可以確認:
- **現況表**: 數據是否重複更新(看 ID 是否相同但時間不同)
- **歷史表**: 數據是否只新增(每一行都是獨立事件)
---
## 常見問題
### Q1: 啟動失敗,顯示 "找不到 Python"
**解決方法**:
1. 確認已安裝 Python 3.11 或更高版本
2. 執行 `python --version` 檢查
3. 如未安裝,請從 https://www.python.org/downloads/ 下載
### Q2: 啟動失敗,顯示 "找不到虛擬環境"
**解決方法**:
1. 先執行 `scripts\0_初始化環境.bat` 初始化環境
2. 或手動執行: `python -m venv venv`
### Q3: 查詢失敗,顯示 "數據庫連接失敗"
**原因**:
- 數據庫服務器未連接
- 網絡問題
- 數據庫帳號密碼錯誤
**解決方法**:
1. 檢查網絡連接
2. 確認 `apps\table_data_viewer.py` 中的數據庫配置是否正確
3. 聯繫數據庫管理員確認帳號狀態
### Q4: 查詢很慢或超時
**原因**:
- 查詢的表數據量太大
- 沒有使用時間範圍過濾
**說明**:
- 工具會自動對大表使用時間欄位排序和 ROWNUM 限制
- 如果仍然很慢,說明該表確實數據量非常大
- 這正好驗證了為什麼開發報表時必須加時間範圍限制
### Q5: 如何停止服務器?
**方法**:
- 在命令提示字元窗口按 `Ctrl+C`
- 或直接關閉命令提示字元窗口
---
## 數據隱私提醒
⚠️ **注意**:
- 本工具連接生產數據庫(只讀權限)
- 請勿將查詢到的敏感數據外傳
- 使用完畢後請關閉服務器
---
## 技術支援
如有問題,請參考:
- [README.md](../README.md) - 專案總覽
- [System_Architecture_Design.md](System_Architecture_Design.md) - 系統架構文檔
- [MES_Core_Tables_Analysis_Report.md](MES_Core_Tables_Analysis_Report.md) - 表分析報告
---
**版本**: 1.0
**最後更新**: 2026-01-14

127
docs/開始使用.txt Normal file
View File

@@ -0,0 +1,127 @@
===============================================
MES 數據查詢工具 - 快速開始指南
===============================================
您好!數據查詢工具已經準備就緒。
📋 現在可以做什麼?
===============================================
✅ 立即可用:查看各表的實際資料
- 雙擊運行: scripts\0_初始化環境.bat首次使用
- 雙擊運行: scripts\啟動Dashboard.bat
- 訪問: http://localhost:5000上方 Tab 可切換頁面)
✅ 已完成的文檔:
1. README.md - 專案總覽
2. docs\System_Architecture_Design.md - 系統架構設計v1.1
3. docs\MES_Core_Tables_Analysis_Report.md - 表分析報告
4. docs\MES_Database_Reference.md - 數據庫參考(已更新)
5. docs\使用說明.md - 查詢工具使用指南
📊 查詢工具功能
===============================================
- 按表性質分類(現況表/歷史表/輔助表)
- 查看各表最後 1000 筆資料
- 大表自動按時間欄位排序
- 美觀的 Web UI 界面
- 即時顯示欄位和數據
🔍 關鍵發現
===============================================
1. 表性質分類:
- 現況快照表4張: WIP, RESOURCE, CONTAINER, JOB
- 歷史累積表10張: RESOURCESTATUS, LOTWIPHISTORY 等
- 輔助表2張
2. 重要認知更新:
⚠️ DW_MES_WIP 雖名為"在制品表",但包含 7700 萬行歷史數據
⚠️ DW_MES_RESOURCESTATUS 記錄狀態變更,需用兩個時間欄位計算持續時間
3. 查詢優化鐵律:
⚠️ 所有超過 1000 萬行的表,查詢時必須加時間範圍限制!
- 儀表板查詢: 最近 7 天
- 報表查詢: 最多 30 天
- 歷史趨勢: 最多 90 天
📂 專案文件結構
===============================================
DashBoard/
├── README.md ← 專案總覽
├── docs/ ← 文件
│ ├── 開始使用.txt ← 您正在閱讀的文件
│ ├── 使用說明.md ← 查詢工具使用指南
│ ├── System_Architecture_Design.md ← 系統架構設計
│ ├── MES_Core_Tables_Analysis_Report.md ← 表分析報告
│ └── MES_Database_Reference.md ← 數據庫參考
├── scripts/ ← 啟動腳本
│ ├── 0_初始化環境.bat ← 首次使用請執行
│ ├── 啟動Dashboard.bat ← 統一入口
│ └── 啟動數據查詢工具.bat ← 相容入口
├── apps/ ← 可執行應用
│ ├── portal.py ← 統一入口主程式
│ ├── table_data_viewer.py ← 查詢工具後端
│ ├── wip_report.py ← WIP 報表後端
│ ├── 快速啟動.py ← Python 直接啟動
│ └── templates\portal.html ← 統一入口前端
🚀 下一步建議
===============================================
現在請執行:
1. ✅ 初始化環境(首次使用)
雙擊: scripts\0_初始化環境.bat
2. ✅ 啟動報表入口
雙擊: scripts\啟動Dashboard.bat
3. ✅ 查看實際資料
訪問: http://localhost:5000
用上方 Tab 切換頁面
4. ⏳ 提供 Power BI 報表截圖
確認儀表板與報表的具體設計
5. ⏳ 選擇優先開發的業務場景
從 8 個場景中選擇 3-5 個優先實現
⏳ 待確認事項
===============================================
1. Power BI 報表截圖 - 用於前端 UI 設計參考
2. 具體報表類型 - 確認要開發哪些報表
3. 部署環境 - 是否有專用服務器
4. 並發用戶數 - 預計同時使用人數
💡 建議的 8 個核心業務場景
===============================================
1. 在制品WIP看板
2. 設備稼動率OEE報表 ⭐
3. 批次生產履歷追溯
4. 工序 Cycle Time 分析
5. 設備產出與效率分析
6. Hold 批次分析
7. 設備維修工單進度追蹤
8. 良率分析
📞 獲取幫助
===============================================
查看詳細文檔:
- docs\使用說明.md - 如何使用查詢工具
- README.md - 專案完整說明
- docs\System_Architecture_Design.md - 技術架構文檔
===============================================
準備好開始了嗎?
雙擊運行: scripts\0_初始化環境.bat
===============================================

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
oracledb>=2.0.0
flask>=3.0.0
pandas>=2.0.0

View File

@@ -0,0 +1,57 @@
@echo off
setlocal EnableDelayedExpansion
echo ========================================
echo Initialize MES Dashboard Environment
echo ========================================
echo.
set "ROOT=%~dp0.."
REM Check Python
python --version >nul 2>&1
if errorlevel 1 (
echo [ERROR] Python not found
echo Please install Python 3.11+
echo Download: https://www.python.org/downloads/
pause
exit /b 1
)
echo [1/3] Checking virtual environment...
if exist "%ROOT%\venv\Scripts\python.exe" (
echo [OK] Virtual environment exists
) else (
echo Creating virtual environment...
python -m venv "%ROOT%\venv"
if errorlevel 1 (
echo [ERROR] Failed to create venv
pause
exit /b 1
)
echo [OK] Virtual environment created
)
echo.
echo [2/3] Installing dependencies...
"%ROOT%\venv\Scripts\pip.exe" install -r "%ROOT%\requirements.txt"
if errorlevel 1 (
echo [ERROR] Failed to install dependencies
pause
exit /b 1
)
echo.
echo [3/3] Verifying packages...
"%ROOT%\venv\Scripts\python.exe" -c "import oracledb; print('[OK] oracledb installed')"
"%ROOT%\venv\Scripts\python.exe" -c "import flask; print('[OK] Flask installed')"
"%ROOT%\venv\Scripts\python.exe" -c "import pandas; print('[OK] Pandas installed')"
echo.
echo ========================================
echo Initialization Complete!
echo ========================================
echo.
echo Run "scripts\啟動Dashboard.bat" to start server
echo.
pause

View File

@@ -0,0 +1,36 @@
@echo off
setlocal EnableDelayedExpansion
echo ========================================
echo MES Dashboard Portal
echo ========================================
echo.
set "ROOT=%~dp0.."
REM Check for python.exe in different locations (standard venv or conda)
if exist "%ROOT%\venv\Scripts\python.exe" (
set "PYTHON=%ROOT%\venv\Scripts\python.exe"
) else if exist "%ROOT%\venv\python.exe" (
set "PYTHON=%ROOT%\venv\python.exe"
) else (
echo [ERROR] Virtual environment not found
echo Please run initialization script first
echo.
pause
exit /b 1
)
echo Starting server...
echo URL: http://localhost:5000
echo Press Ctrl+C to stop
echo.
echo ========================================
echo.
"%PYTHON%" "%ROOT%\apps\portal.py"
echo.
echo ========================================
echo Server stopped
pause

View File

@@ -0,0 +1,9 @@
@echo off
chcp 65001 >nul
echo ========================================
echo WIP 報表已整合到統一入口
echo ========================================
echo.
echo 將轉到: scripts\啟動Dashboard.bat
echo.
call "%~dp0啟動Dashboard.bat"

View File

@@ -0,0 +1,9 @@
@echo off
chcp 65001 >nul
echo ========================================
echo 數據查詢工具已整合到統一入口
echo ========================================
echo.
echo 將轉到: scripts\啟動Dashboard.bat
echo.
call "%~dp0啟動Dashboard.bat"

View File

@@ -0,0 +1,339 @@
"""
生成 MES 数据库参考文档
用于报表开发参考
"""
import json
from pathlib import Path
from datetime import datetime
# 读取表结构信息
ROOT_DIR = Path(__file__).resolve().parent.parent
DATA_FILE = ROOT_DIR / 'data' / 'table_schema_info.json'
with open(DATA_FILE, 'r', encoding='utf-8') as f:
table_info = json.load(f)
# 表用途描述(根据表名推断)
TABLE_DESCRIPTIONS = {
'DW_MES_CONTAINER': '容器/批次主檔 - 目前在製容器狀態、數量與流程資訊',
'DW_MES_HOLDRELEASEHISTORY': 'Hold/Release 歷史表 - 批次停工與解除紀錄',
'DW_MES_JOB': '設備維修工單表 - 維修工單的當前狀態與流程',
'DW_MES_LOTREJECTHISTORY': '批次不良/報廢歷史表 - 不良原因與數量',
'DW_MES_LOTWIPDATAHISTORY': '在製數據採集歷史表 - 製程量測/參數紀錄',
'DW_MES_LOTWIPHISTORY': '在製流轉歷史表 - 批次進出站與流程軌跡',
'DW_MES_MAINTENANCE': '設備保養/維護紀錄表 - 保養計畫與點檢數據',
'DW_MES_PARTREQUESTORDER': '維修用料請求表 - 維修/設備零件請領',
'DW_MES_PJ_COMBINEDASSYLOTS': '併批紀錄表 - 合批/合併批次關聯與數量資訊',
'DW_MES_RESOURCESTATUS': '設備狀態變更歷史表 - 狀態切換與原因',
'DW_MES_RESOURCESTATUS_SHIFT': '設備狀態班次彙總表 - 班次級狀態/工時',
'DW_MES_WIP': '在製品現況表(含歷史累積)- 當前 WIP 狀態/數量',
'DW_MES_HM_LOTMOVEOUT': '批次出站事件歷史表 - 出站/移出交易',
'DW_MES_JOBTXNHISTORY': '維修工單交易歷史表 - 工單狀態變更紀錄',
'DW_MES_LOTMATERIALSHISTORY': '批次物料消耗歷史表 - 用料與批次關聯',
'DW_MES_RESOURCE': '資源表 - 設備/載具等資源基本資料OBJECTCATEGORY=ASSEMBLY 時RESOURCENAME 為設備編號)'
}
# 常见字段说明
COMMON_FIELD_NOTES = {
'ID': '唯一标识符',
'NAME': '名称',
'STATUS': '状态',
'TIMESTAMP': '时间戳',
'CREATEDATE': '创建日期',
'UPDATEDATE': '更新日期',
'LOTID': '批次ID',
'CONTAINERID': '容器ID',
'RESOURCEID': '资源ID',
'EQUIPMENTID': '设备ID',
'OPERATIONID': '工序ID',
'JOBID': '工单ID',
'PRODUCTID': '产品ID',
'CUSTOMERID': '客户ID',
'QTY': '数量',
'QUANTITY': '数量'
}
def generate_markdown():
"""生成 Markdown 文档"""
md = []
# 标题和简介
md.append("# MES 数据库报表开发参考文档\n")
md.append(f"**生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
md.append("---\n")
# 目录
md.append("## 目录\n")
md.append("1. [数据库连接信息](#数据库连接信息)")
md.append("2. [数据库概览](#数据库概览)")
md.append("3. [表结构详细说明](#表结构详细说明)")
md.append("4. [报表开发注意事项](#报表开发注意事项)")
md.append("5. [常用查询示例](#常用查询示例)\n")
md.append("---\n")
# 1. 数据库连接信息
md.append("## 数据库连接信息\n")
md.append("### 连接参数\n")
md.append("| 参数 | 值 |")
md.append("|------|------|")
md.append("| 数据库类型 | Oracle Database 19c Enterprise Edition |")
md.append("| 主机地址 | 10.1.1.58 |")
md.append("| 端口 | 1521 |")
md.append("| 服务名 | DWDB |")
md.append("| 用户名 | MBU1_R |")
md.append("| 密码 | Pj2481mbu1 |\n")
md.append("### Python 连接示例\n")
md.append("```python")
md.append("import oracledb")
md.append("")
md.append("# 连接配置")
md.append("DB_CONFIG = {")
md.append(" 'user': 'MBU1_R',")
md.append(" 'password': 'Pj2481mbu1',")
md.append(" 'dsn': '(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=10.1.1.58)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=DWDB)))'")
md.append("}")
md.append("")
md.append("# 建立连接")
md.append("connection = oracledb.connect(**DB_CONFIG)")
md.append("cursor = connection.cursor()")
md.append("")
md.append("# 执行查询")
md.append("cursor.execute('SELECT * FROM DW_MES_WIP WHERE ROWNUM <= 10')")
md.append("results = cursor.fetchall()")
md.append("")
md.append("# 关闭连接")
md.append("cursor.close()")
md.append("connection.close()")
md.append("```\n")
md.append("### JDBC 连接字符串\n")
md.append("```")
md.append("jdbc:oracle:thin:@10.1.1.58:1521:DWDB")
md.append("```\n")
# 2. 数据库概览
md.append("---\n")
md.append("## 数据库概览\n")
md.append("### 表统计信息\n")
md.append("| # | 表名 | 用途 | 数据量 |")
md.append("|---|------|------|--------|")
for idx, (table_name, info) in enumerate(sorted(table_info.items()), 1):
if 'error' not in info:
row_count = f"{info['row_count']:,}"
description = TABLE_DESCRIPTIONS.get(table_name, '待补充')
md.append(f"| {idx} | `{table_name}` | {description} | {row_count} |")
md.append("")
# 计算总数据量
total_rows = sum(info['row_count'] for info in table_info.values() if 'error' not in info)
md.append(f"**总数据量**: {total_rows:,}\n")
# 3. 表结构详细说明
md.append("---\n")
md.append("## 表结构详细说明\n")
for table_name in sorted(table_info.keys()):
info = table_info[table_name]
if 'error' in info:
continue
md.append(f"### {table_name}\n")
# 表说明
md.append(f"**用途**: {TABLE_DESCRIPTIONS.get(table_name, '待补充')}\n")
md.append(f"**数据量**: {info['row_count']:,}\n")
if info.get('table_comment'):
md.append(f"**表注释**: {info['table_comment']}\n")
# 字段列表
md.append("#### 字段列表\n")
md.append("| # | 字段名 | 数据类型 | 长度 | 可空 | 说明 |")
md.append("|---|--------|----------|------|------|------|")
schema = info.get('schema', [])
for col in schema:
col_num = col['column_id']
col_name = col['column_name']
# 构建数据类型显示
if col['data_type'] in ['VARCHAR2', 'CHAR']:
data_type = f"{col['data_type']}({col['data_length']})"
elif col['data_type'] == 'NUMBER' and col['data_precision']:
if col['data_scale']:
data_type = f"NUMBER({col['data_precision']},{col['data_scale']})"
else:
data_type = f"NUMBER({col['data_precision']})"
else:
data_type = col['data_type']
nullable = "" if col['nullable'] == 'Y' else ""
# 获取字段说明
column_comments = info.get('column_comments', {})
comment = column_comments.get(col_name, '')
# 如果没有注释,尝试从常见字段说明中获取
if not comment:
for key, value in COMMON_FIELD_NOTES.items():
if key in col_name:
comment = value
break
md.append(f"| {col_num} | `{col_name}` | {data_type} | {col.get('data_length', '-')} | {nullable} | {comment} |")
md.append("")
# 索引信息
indexes = info.get('indexes', [])
if indexes:
md.append("#### 索引\n")
md.append("| 索引名 | 类型 | 字段 |")
md.append("|--------|------|------|")
for idx_info in indexes:
idx_type = "唯一索引" if idx_info[1] == 'UNIQUE' else "普通索引"
md.append(f"| `{idx_info[0]}` | {idx_type} | {idx_info[2]} |")
md.append("")
md.append("---\n")
# 4. 报表开发注意事项
md.append("## 报表开发注意事项\n")
md.append("### 性能优化建议\n")
md.append("1. **大数据量表查询优化**")
md.append(" - 以下表数据量较大,查询时务必添加时间范围限制:")
large_tables = [(name, info['row_count']) for name, info in table_info.items()
if 'error' not in info and info['row_count'] > 10000000]
large_tables.sort(key=lambda x: x[1], reverse=True)
for table_name, count in large_tables:
md.append(f" - `{table_name}`: {count:,}")
md.append("")
md.append("2. **索引使用**")
md.append(" - 查询时尽量使用已建立索引的字段作为查询条件")
md.append(" - 避免在索引字段上使用函数,会导致索引失效")
md.append("")
md.append("3. **连接池配置**")
md.append(" - 建议使用连接池管理数据库连接")
md.append(" - 推荐连接池大小5-10 个连接")
md.append("")
md.append("4. **查询超时设置**")
md.append(" - 建议设置查询超时时间为 30-60 秒")
md.append(" - 避免长时间运行的查询影响系统性能")
md.append("")
md.append("### 数据时效性\n")
md.append("- **实时数据表**: `DW_MES_WIP`(含歷史累積), `DW_MES_RESOURCESTATUS`")
md.append("- **历史数据表**: 带有 `HISTORY` 后缀的表")
md.append("- **主数据表**: `DW_MES_RESOURCE`, `DW_MES_CONTAINER`")
md.append("")
md.append("### 常用时间字段\n")
md.append("大多数历史表包含以下时间相关字段:")
md.append("- `CREATEDATE` / `CREATETIMESTAMP`: 记录创建时间")
md.append("- `UPDATEDATE` / `UPDATETIMESTAMP`: 记录更新时间")
md.append("- `TRANSACTIONDATE`: 交易发生时间")
md.append("")
md.append("### 数据权限\n")
md.append("- 当前账号 `MBU1_R` 为只读账号")
md.append("- 仅可执行 SELECT 查询")
md.append("- 无法进行 INSERT, UPDATE, DELETE 操作")
md.append("")
# 5. 常用查询示例
md.append("---\n")
md.append("## 常用查询示例\n")
md.append("### 1. 查询当前在制品数量\n")
md.append("```sql")
md.append("SELECT COUNT(*) as WIP_COUNT")
md.append("FROM DW_MES_WIP")
md.append("WHERE CURRENTSTATUSID IS NOT NULL;")
md.append("```\n")
md.append("### 2. 查询设备状态统计\n")
md.append("```sql")
md.append("SELECT")
md.append(" CURRENTSTATUSID,")
md.append(" COUNT(*) as COUNT")
md.append("FROM DW_MES_RESOURCESTATUS")
md.append("GROUP BY CURRENTSTATUSID")
md.append("ORDER BY COUNT DESC;")
md.append("```\n")
md.append("### 3. 查询最近 7 天的批次历史\n")
md.append("```sql")
md.append("SELECT *")
md.append("FROM DW_MES_LOTWIPHISTORY")
md.append("WHERE CREATEDATE >= SYSDATE - 7")
md.append("ORDER BY CREATEDATE DESC;")
md.append("```\n")
md.append("### 4. 查询工单完成情况\n")
md.append("```sql")
md.append("SELECT")
md.append(" JOBID,")
md.append(" JOBSTATUS,")
md.append(" COUNT(*) as COUNT")
md.append("FROM DW_MES_JOB")
md.append("GROUP BY JOBID, JOBSTATUS")
md.append("ORDER BY JOBID;")
md.append("```\n")
md.append("### 5. 按日期统计生产数量\n")
md.append("```sql")
md.append("SELECT")
md.append(" TRUNC(CREATEDATE) as PRODUCTION_DATE,")
md.append(" COUNT(*) as LOT_COUNT")
md.append("FROM DW_MES_HM_LOTMOVEOUT")
md.append("WHERE CREATEDATE >= SYSDATE - 30")
md.append("GROUP BY TRUNC(CREATEDATE)")
md.append("ORDER BY PRODUCTION_DATE DESC;")
md.append("```\n")
md.append("### 6. 联表查询示例(批次与容器)\n")
md.append("```sql")
md.append("SELECT")
md.append(" w.LOTID,")
md.append(" w.CONTAINERNAME,")
md.append(" c.CURRENTSTATUSID,")
md.append(" c.CUSTOMERID")
md.append("FROM DW_MES_WIP w")
md.append("LEFT JOIN DW_MES_CONTAINER c ON w.CONTAINERID = c.CONTAINERID")
md.append("WHERE w.CREATEDATE >= SYSDATE - 1")
md.append("ORDER BY w.CREATEDATE DESC;")
md.append("```\n")
md.append("---\n")
md.append("## 附录\n")
md.append("### 文档更新记录\n")
md.append(f"- {datetime.now().strftime('%Y-%m-%d')}: 初始版本创建")
md.append("")
md.append("### 联系方式\n")
md.append("如有疑问或需要补充信息,请联系数据库管理员。\n")
return '\n'.join(md)
if __name__ == "__main__":
print("Generating documentation...")
markdown_content = generate_markdown()
output_file = ROOT_DIR / 'docs' / 'MES_Database_Reference.md'
with open(output_file, 'w', encoding='utf-8') as f:
f.write(markdown_content)
print(f"[OK] Documentation generated: {output_file}")

187
tools/query_table_schema.py Normal file
View File

@@ -0,0 +1,187 @@
"""
查询 MES 表结构信息脚本
用于生成报表开发参考文档
"""
import sys
import io
import oracledb
import json
from pathlib import Path
# 设置 UTF-8 编码输出
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
# 数据库连接信息
DB_CONFIG = {
'user': 'MBU1_R',
'password': 'Pj2481mbu1',
'dsn': '(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=10.1.1.58)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=DWDB)))'
}
# MES 表列表
MES_TABLES = [
'DW_MES_CONTAINER',
'DW_MES_HOLDRELEASEHISTORY',
'DW_MES_JOB',
'DW_MES_LOTREJECTHISTORY',
'DW_MES_LOTWIPDATAHISTORY',
'DW_MES_LOTWIPHISTORY',
'DW_MES_MAINTENANCE',
'DW_MES_PARTREQUESTORDER',
'DW_MES_PJ_COMBINEDASSYLOTS',
'DW_MES_RESOURCESTATUS',
'DW_MES_RESOURCESTATUS_SHIFT',
'DW_MES_WIP',
'DW_MES_HM_LOTMOVEOUT',
'DW_MES_JOBTXNHISTORY',
'DW_MES_LOTMATERIALSHISTORY',
'DW_MES_RESOURCE'
]
def get_table_schema(cursor, table_name):
"""获取表的结构信息"""
query = """
SELECT
COLUMN_NAME,
DATA_TYPE,
DATA_LENGTH,
DATA_PRECISION,
DATA_SCALE,
NULLABLE,
DATA_DEFAULT,
COLUMN_ID
FROM ALL_TAB_COLUMNS
WHERE TABLE_NAME = :table_name
ORDER BY COLUMN_ID
"""
cursor.execute(query, table_name=table_name)
columns = cursor.fetchall()
schema = []
for col in columns:
col_info = {
'column_name': col[0],
'data_type': col[1],
'data_length': col[2],
'data_precision': col[3],
'data_scale': col[4],
'nullable': col[5],
'default_value': col[6],
'column_id': col[7]
}
schema.append(col_info)
return schema
def get_table_comments(cursor, table_name):
"""获取表和列的注释"""
# 获取表注释
cursor.execute("""
SELECT COMMENTS
FROM ALL_TAB_COMMENTS
WHERE TABLE_NAME = :table_name
""", table_name=table_name)
table_comment = cursor.fetchone()
# 获取列注释
cursor.execute("""
SELECT COLUMN_NAME, COMMENTS
FROM ALL_COL_COMMENTS
WHERE TABLE_NAME = :table_name
ORDER BY COLUMN_NAME
""", table_name=table_name)
column_comments = {row[0]: row[1] for row in cursor.fetchall()}
return table_comment[0] if table_comment else None, column_comments
def get_table_indexes(cursor, table_name):
"""获取表的索引信息"""
query = """
SELECT
i.INDEX_NAME,
i.UNIQUENESS,
LISTAGG(ic.COLUMN_NAME, ', ') WITHIN GROUP (ORDER BY ic.COLUMN_POSITION) as COLUMNS
FROM ALL_INDEXES i
JOIN ALL_IND_COLUMNS ic ON i.INDEX_NAME = ic.INDEX_NAME AND i.TABLE_NAME = ic.TABLE_NAME
WHERE i.TABLE_NAME = :table_name
GROUP BY i.INDEX_NAME, i.UNIQUENESS
ORDER BY i.INDEX_NAME
"""
cursor.execute(query, table_name=table_name)
return cursor.fetchall()
def get_sample_data(cursor, table_name, limit=5):
"""获取表的示例数据"""
try:
cursor.execute(f"SELECT * FROM {table_name} WHERE ROWNUM <= {limit}")
columns = [col[0] for col in cursor.description]
rows = cursor.fetchall()
return columns, rows
except Exception as e:
return None, str(e)
def main():
"""主函数"""
print("Connecting to database...")
connection = oracledb.connect(**DB_CONFIG)
cursor = connection.cursor()
all_table_info = {}
print(f"\nQuerying schema information for {len(MES_TABLES)} tables...\n")
for idx, table_name in enumerate(MES_TABLES, 1):
print(f"[{idx}/{len(MES_TABLES)}] Processing {table_name}...")
try:
# 获取表结构
schema = get_table_schema(cursor, table_name)
# 获取注释
table_comment, column_comments = get_table_comments(cursor, table_name)
# 获取索引
indexes = get_table_indexes(cursor, table_name)
# 获取行数
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
row_count = cursor.fetchone()[0]
# 获取示例数据
sample_columns, sample_data = get_sample_data(cursor, table_name, limit=3)
all_table_info[table_name] = {
'table_comment': table_comment,
'row_count': row_count,
'schema': schema,
'column_comments': column_comments,
'indexes': indexes,
'sample_columns': sample_columns,
'sample_data': sample_data
}
except Exception as e:
print(f" Error: {str(e)}")
all_table_info[table_name] = {'error': str(e)}
# 保存到 JSON 文件
output_file = Path(__file__).resolve().parent.parent / 'data' / 'table_schema_info.json'
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(all_table_info, f, ensure_ascii=False, indent=2, default=str)
print(f"\n[OK] Schema information saved to {output_file}")
cursor.close()
connection.close()
print("[OK] Connection closed")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,136 @@
"""
Oracle Database Connection Test Script
测试连接到 DWDB 数据库并验证 MES 表访问权限
"""
import sys
import io
import oracledb
from datetime import datetime
# 设置 UTF-8 编码输出
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
# 数据库连接信息
DB_CONFIG = {
'user': 'MBU1_R',
'password': 'Pj2481mbu1',
'dsn': '(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=10.1.1.58)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=DWDB)))'
}
# MES 表列表
MES_TABLES = [
'DW_MES_CONTAINER',
'DW_MES_HOLDRELEASEHISTORY',
'DW_MES_JOB',
'DW_MES_LOTREJECTHISTORY',
'DW_MES_LOTWIPDATAHISTORY',
'DW_MES_LOTWIPHISTORY',
'DW_MES_MAINTENANCE',
'DW_MES_PARTREQUESTORDER',
'DW_MES_PJ_COMBINEDASSYLOTS',
'DW_MES_RESOURCESTATUS',
'DW_MES_RESOURCESTATUS_SHIFT',
'DW_MES_WIP',
'DW_MES_HM_LOTMOVEOUT',
'DW_MES_JOBTXNHISTORY',
'DW_MES_LOTMATERIALSHISTORY',
'DW_MES_RESOURCE'
]
def test_connection():
"""测试数据库连接"""
print("=" * 60)
print("Oracle Database Connection Test")
print("=" * 60)
print(f"Test Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Host: 10.1.1.58:1521")
print(f"Service Name: DWDB")
print(f"User: {DB_CONFIG['user']}")
print("=" * 60)
try:
# 尝试连接数据库
print("\n[1/3] Attempting to connect to database...")
connection = oracledb.connect(**DB_CONFIG)
print("[OK] Connection successful!")
# 获取数据库版本信息
print("\n[2/3] Retrieving database version...")
cursor = connection.cursor()
cursor.execute("SELECT * FROM v$version WHERE banner LIKE 'Oracle%'")
version = cursor.fetchone()
if version:
print(f"[OK] Database Version: {version[0]}")
# 测试每个表的访问权限
print("\n[3/3] Testing access to MES tables...")
print("-" * 60)
accessible_tables = []
inaccessible_tables = []
for table_name in MES_TABLES:
try:
# 尝试查询表的行数
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
count = cursor.fetchone()[0]
print(f"[OK] {table_name:<35} - {count:,} rows")
accessible_tables.append(table_name)
except oracledb.DatabaseError as e:
error_obj, = e.args
print(f"[FAIL] {table_name:<35} - Error: {error_obj.message}")
inaccessible_tables.append((table_name, error_obj.message))
# 汇总结果
print("\n" + "=" * 60)
print("Test Summary")
print("=" * 60)
print(f"Total tables tested: {len(MES_TABLES)}")
print(f"Accessible tables: {len(accessible_tables)}")
print(f"Inaccessible tables: {len(inaccessible_tables)}")
if inaccessible_tables:
print("\nInaccessible Tables:")
for table, error in inaccessible_tables:
print(f" - {table}: {error}")
# 关闭连接
cursor.close()
connection.close()
print("\n[OK] Connection closed successfully")
return len(inaccessible_tables) == 0
except oracledb.DatabaseError as e:
error_obj, = e.args
print(f"\n[FAIL] Database Error: {error_obj.message}")
print(f" Error Code: {error_obj.code}")
return False
except Exception as e:
print(f"\n[FAIL] Unexpected Error: {str(e)}")
return False
def main():
"""主函数"""
try:
success = test_connection()
print("\n" + "=" * 60)
if success:
print("[SUCCESS] All tests passed successfully!")
else:
print("[WARNING] Some tests failed. Please check the output above.")
print("=" * 60)
except KeyboardInterrupt:
print("\n\nTest interrupted by user.")
except Exception as e:
print(f"\n\nFatal error: {str(e)}")
if __name__ == "__main__":
main()