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:
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal 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
253
README.md
Normal 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
1536
apps/portal.py
Normal file
File diff suppressed because it is too large
Load Diff
270
apps/table_data_viewer.py
Normal file
270
apps/table_data_viewer.py
Normal 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
606
apps/templates/index.html
Normal 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}')">×</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
140
apps/templates/portal.html
Normal 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>
|
||||
1057
apps/templates/resource_status.html
Normal file
1057
apps/templates/resource_status.html
Normal file
File diff suppressed because it is too large
Load Diff
940
apps/templates/wip_report.html
Normal file
940
apps/templates/wip_report.html
Normal 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
401
apps/wip_report.py
Normal 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
29
apps/快速啟動.py
Normal 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
10352
data/table_schema_info.json
Normal file
File diff suppressed because it is too large
Load Diff
1845
docs/MES_Core_Tables_Analysis_Report.md
Normal file
1845
docs/MES_Core_Tables_Analysis_Report.md
Normal file
File diff suppressed because it is too large
Load Diff
1221
docs/MES_Database_Reference.md
Normal file
1221
docs/MES_Database_Reference.md
Normal file
File diff suppressed because it is too large
Load Diff
1446
docs/System_Architecture_Design.md
Normal file
1446
docs/System_Architecture_Design.md
Normal file
File diff suppressed because it is too large
Load Diff
282
docs/WIP報表說明.md
Normal file
282
docs/WIP報表說明.md
Normal 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
210
docs/使用說明.md
Normal 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
127
docs/開始使用.txt
Normal 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
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
oracledb>=2.0.0
|
||||
flask>=3.0.0
|
||||
pandas>=2.0.0
|
||||
57
scripts/0_初始化環境.bat
Normal file
57
scripts/0_初始化環境.bat
Normal 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
|
||||
36
scripts/啟動Dashboard.bat
Normal file
36
scripts/啟動Dashboard.bat
Normal 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
|
||||
9
scripts/啟動WIP報表.bat
Normal file
9
scripts/啟動WIP報表.bat
Normal file
@@ -0,0 +1,9 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo ========================================
|
||||
echo WIP 報表已整合到統一入口
|
||||
echo ========================================
|
||||
echo.
|
||||
echo 將轉到: scripts\啟動Dashboard.bat
|
||||
echo.
|
||||
call "%~dp0啟動Dashboard.bat"
|
||||
9
scripts/啟動數據查詢工具.bat
Normal file
9
scripts/啟動數據查詢工具.bat
Normal file
@@ -0,0 +1,9 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo ========================================
|
||||
echo 數據查詢工具已整合到統一入口
|
||||
echo ========================================
|
||||
echo.
|
||||
echo 將轉到: scripts\啟動Dashboard.bat
|
||||
echo.
|
||||
call "%~dp0啟動Dashboard.bat"
|
||||
339
tools/generate_documentation.py
Normal file
339
tools/generate_documentation.py
Normal 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
187
tools/query_table_schema.py
Normal 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()
|
||||
|
||||
136
tools/test_oracle_connection.py
Normal file
136
tools/test_oracle_connection.py
Normal 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()
|
||||
Reference in New Issue
Block a user