|
<!DOCTYPE html>
|
|
<html lang="zh-Hant">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>MES系統 - 雷刻檔設定</title>
|
|
<style>
|
|
:root{
|
|
--blue-900:#1e3a8a; --blue-800:#1e40af; --blue-700:#1d4ed8; --blue-600:#2563eb; --blue-500:#3b82f6;
|
|
--gray-50:#f8fafc; --gray-100:#f1f5f9; --gray-200:#e2e8f0; --gray-300:#cbd5e1; --gray-500:#64748b; --gray-600:#475569; --gray-800:#1e293b;
|
|
--green-600:#16a34a; --green-50:#f0fdf4;
|
|
--amber-600:#d97706; --amber-50:#fffbeb;
|
|
--purple-600:#7c3aed; --purple-50:#f5f3ff;
|
|
--teal-600:#0d9488; --teal-50:#f0fdfa;
|
|
--red-600:#dc2626; --red-50:#fef2f2;
|
|
}
|
|
*{box-sizing:border-box;}
|
|
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang TC","Microsoft JhengHei",sans-serif;background:var(--gray-100);color:var(--gray-800);display:flex;min-height:100vh;font-size:14px;}
|
|
|
|
.sidebar{width:220px;background:linear-gradient(180deg,var(--blue-900),var(--blue-800));color:#fff;flex-shrink:0;display:flex;flex-direction:column;}
|
|
.sidebar-header{padding:16px 18px;font-weight:700;font-size:16px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid rgba(255,255,255,.12);}
|
|
.sidebar-header span.ver{font-size:11px;opacity:.6;font-weight:400;}
|
|
.nav-item{padding:12px 18px;font-size:13.5px;cursor:default;opacity:.85;border-left:3px solid transparent;}
|
|
.nav-item.active{background:rgba(255,255,255,.1);opacity:1;border-left-color:#fff;font-weight:600;}
|
|
.nav-sub{padding:10px 18px 10px 40px;font-size:13px;opacity:.85;}
|
|
.nav-sub.current{background:rgba(255,255,255,.15);opacity:1;font-weight:600;border-left:3px solid #fff;}
|
|
.sidebar-footer{margin-top:auto;padding:14px 18px;border-top:1px solid rgba(255,255,255,.12);display:flex;align-items:center;gap:10px;font-size:13px;}
|
|
.avatar{width:28px;height:28px;border-radius:50%;background:rgba(255,255,255,.2);display:flex;align-items:center;justify-content:center;font-size:12px;}
|
|
|
|
.main{flex:1;display:flex;flex-direction:column;min-width:0;}
|
|
.topbar{background:#fff;padding:16px 28px;font-size:19px;font-weight:700;border-bottom:1px solid var(--gray-200);}
|
|
.topbar .breadcrumb{font-size:12.5px;font-weight:400;color:var(--gray-500);margin-top:4px;}
|
|
|
|
.content{padding:20px 28px 40px;display:flex;flex-direction:column;gap:20px;}
|
|
|
|
.card{background:#fff;border-radius:10px;border:1px solid var(--gray-200);box-shadow:0 1px 2px rgba(0,0,0,.03);}
|
|
.card-head{padding:16px 20px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--gray-200);gap:16px;}
|
|
.card-head h2{margin:0;font-size:15.5px;}
|
|
.card-head p{margin:3px 0 0;font-size:12.5px;color:var(--gray-500);font-weight:400;}
|
|
|
|
.btn{border:none;border-radius:6px;padding:9px 16px;font-size:13.5px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:filter .15s;white-space:nowrap;}
|
|
.btn:hover{filter:brightness(0.93);}
|
|
.btn:active{filter:brightness(0.85);}
|
|
.btn:disabled{opacity:.45;cursor:not-allowed;filter:none;}
|
|
.btn-green{background:var(--green-600);color:#fff;}
|
|
.btn-blue{background:var(--blue-600);color:#fff;}
|
|
.btn-gray{background:var(--gray-200);color:var(--gray-600);}
|
|
.btn-outline{background:#fff;color:var(--blue-600);border:1px solid var(--blue-500);}
|
|
.btn-purple{background:linear-gradient(135deg,#7c3aed,#6d28d9);color:#fff;}
|
|
.btn-sm{padding:6px 12px;font-size:12.5px;}
|
|
|
|
.status-pill{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;font-weight:600;padding:5px 12px;border-radius:999px;}
|
|
.status-ok{background:var(--green-50);color:var(--green-600);}
|
|
.status-fail{background:var(--red-50);color:var(--red-600);}
|
|
.status-dot{width:7px;height:7px;border-radius:50%;background:currentColor;}
|
|
|
|
.field{display:flex;flex-direction:column;gap:5px;}
|
|
.field label{font-size:12.5px;color:var(--gray-600);font-weight:600;}
|
|
.field input[type=text],.field select{padding:9px 12px;border:1px solid var(--gray-300);border-radius:6px;font-size:13.5px;background:#fff;width:100%;}
|
|
.field input:focus,.field select:focus{outline:none;border-color:var(--blue-500);box-shadow:0 0 0 3px rgba(59,130,246,.15);}
|
|
|
|
table{width:100%;border-collapse:collapse;}
|
|
thead th{text-align:left;font-size:12.5px;color:var(--gray-500);font-weight:600;padding:12px 16px;border-bottom:1px solid var(--gray-200);white-space:nowrap;background:#fff;}
|
|
tbody td{padding:14px 16px;border-bottom:1px solid var(--gray-100);font-size:13.5px;vertical-align:middle;}
|
|
tbody tr:hover{background:var(--gray-50);}
|
|
|
|
.tag{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:700;letter-spacing:.3px;white-space:nowrap;}
|
|
.tag-rgp{background:var(--teal-50);color:var(--teal-600);border:1px solid #99f6e4;}
|
|
.tag-ok{background:var(--green-50);color:var(--green-600);border:1px solid #bbf7d0;}
|
|
.tag-rgptest{background:var(--blue-500);color:#fff;border:1px solid var(--blue-500);}
|
|
.tag-oktest{background:var(--amber-50);color:var(--amber-600);border:1px solid #fde68a;}
|
|
.tag-aok{background:var(--purple-50);color:var(--purple-600);border:1px solid #ddd6fe;}
|
|
.tag-aoktest{background:var(--red-50);color:var(--red-600);border:1px solid #fecaca;}
|
|
.tag-custom{background:var(--gray-100);color:var(--gray-600);border:1px solid var(--gray-300);}
|
|
|
|
.remark-input{font-size:12.5px;color:var(--gray-600);padding:8px 10px;border:1px solid var(--gray-300);border-radius:6px;width:240px;background:#fff;}
|
|
.remark-input:focus{outline:none;border-color:var(--blue-500);box-shadow:0 0 0 3px rgba(59,130,246,.15);}
|
|
|
|
.ftps-btn{
|
|
display:flex;flex-direction:column;align-items:flex-start;gap:3px;
|
|
background:#fff;border:1px solid var(--gray-300);border-radius:8px;padding:8px 12px;cursor:pointer;min-width:190px;font-size:12px;
|
|
}
|
|
.ftps-btn:hover{border-color:var(--blue-500);background:#f8fbff;}
|
|
.ftps-btn .ftps-title{font-weight:700;color:var(--gray-700,#334155);display:flex;align-items:center;gap:6px;}
|
|
.ftps-btn .ftps-dest{font-family:Consolas,monospace;font-size:11px;color:var(--gray-500);}
|
|
.ftps-btn .ftps-dest.unset{color:var(--amber-600);}
|
|
|
|
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px 16px;}
|
|
.field.full{grid-column:1 / -1;}
|
|
.pw-wrap{position:relative;}
|
|
.pw-wrap input{padding-right:52px !important;}
|
|
.pw-wrap button{position:absolute;right:8px;top:50%;transform:translateY(-50%);border:none;background:none;color:var(--blue-600);cursor:pointer;font-size:12px;font-weight:600;}
|
|
|
|
.rule-summary-btn{
|
|
display:flex;flex-direction:column;align-items:flex-start;gap:4px;
|
|
background:#fff;border:1px solid var(--gray-300);border-radius:8px;padding:8px 12px;cursor:pointer;min-width:220px;
|
|
}
|
|
.rule-summary-btn:hover{border-color:var(--blue-500);background:#f8fbff;}
|
|
.rule-summary-btn .count-row{display:flex;align-items:center;gap:8px;font-size:12.5px;font-weight:700;color:var(--blue-600);}
|
|
.rule-summary-btn .count-badge{background:var(--blue-600);color:#fff;border-radius:999px;padding:1px 8px;font-size:11px;}
|
|
.rule-summary-btn .preview-pattern{font-family:Consolas,monospace;font-size:11px;color:var(--gray-500);max-width:230px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
|
|
.toggle{position:relative;display:inline-block;width:38px;height:22px;}
|
|
.toggle input{opacity:0;width:0;height:0;}
|
|
.toggle .slider{position:absolute;cursor:pointer;inset:0;background:var(--gray-300);border-radius:999px;transition:.2s;}
|
|
.toggle .slider:before{position:absolute;content:"";height:16px;width:16px;left:3px;top:3px;background:#fff;border-radius:50%;transition:.2s;}
|
|
.toggle input:checked + .slider{background:var(--green-600);}
|
|
.toggle input:checked + .slider:before{transform:translateX(16px);}
|
|
.toggle input:disabled + .slider{opacity:.5;cursor:not-allowed;}
|
|
.remark-input:disabled, .rule-summary-btn:disabled, .ftps-btn:disabled{background:var(--gray-100);color:var(--gray-400,#94a3b8);cursor:not-allowed;opacity:.7;}
|
|
|
|
.row-actions{display:flex;gap:6px;flex-wrap:wrap;}
|
|
.icon-btn{border:1px solid var(--gray-300);background:#fff;color:var(--gray-600);border-radius:6px;padding:6px 10px;font-size:12px;cursor:pointer;}
|
|
.icon-btn:hover{background:var(--gray-50);}
|
|
.icon-btn.danger{color:var(--red-600);border-color:#fecaca;}
|
|
.icon-btn.danger:hover{background:var(--red-50);}
|
|
.icon-btn:disabled{opacity:.35;cursor:not-allowed;}
|
|
|
|
.card-foot{display:flex;justify-content:space-between;align-items:center;padding:14px 20px;border-top:1px solid var(--gray-200);}
|
|
.card-foot .last-saved{font-size:12px;color:var(--gray-500);}
|
|
|
|
/* Rule editor modal */
|
|
.modal-overlay{position:fixed;inset:0;background:rgba(15,23,42,.5);display:none;align-items:center;justify-content:center;z-index:50;padding:20px;}
|
|
.modal-overlay.show{display:flex;}
|
|
.modal{background:#fff;border-radius:12px;width:460px;max-width:92vw;padding:28px 26px;text-align:center;box-shadow:0 20px 50px rgba(0,0,0,.25);}
|
|
.modal.rule-modal{width:920px;max-width:96vw;text-align:left;padding:0;display:flex;flex-direction:column;max-height:88vh;}
|
|
.rule-modal-head{padding:20px 24px;border-bottom:1px solid var(--gray-200);display:flex;justify-content:space-between;align-items:flex-start;gap:12px;}
|
|
.rule-modal-head h3{margin:0 0 4px;font-size:16px;}
|
|
.rule-modal-head p{margin:0;font-size:12.5px;color:var(--gray-500);}
|
|
.rule-modal-body{padding:18px 24px;overflow-y:auto;flex:1;}
|
|
.rule-modal-foot{padding:16px 24px;border-top:1px solid var(--gray-200);display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;}
|
|
|
|
.rule-table{width:100%;border-collapse:collapse;}
|
|
.rule-table th{text-align:left;font-size:11.5px;color:var(--gray-500);font-weight:600;padding:8px 8px;border-bottom:1px solid var(--gray-200);white-space:nowrap;}
|
|
.rule-table td{padding:6px 8px;border-bottom:1px solid var(--gray-100);vertical-align:middle;}
|
|
.rule-input{width:100%;padding:7px 9px;border:1px solid var(--gray-300);border-radius:6px;font-size:12.5px;font-family:Consolas,monospace;background:#fff;}
|
|
.rule-input.wide{min-width:200px;}
|
|
.rule-input.narrow{width:60px;font-family:inherit;text-align:center;}
|
|
.rule-input:focus{outline:none;border-color:var(--blue-500);box-shadow:0 0 0 3px rgba(59,130,246,.15);}
|
|
.rule-input.invalid{border-color:var(--red-600);background:var(--red-50);}
|
|
.rule-order{font-family:Consolas,monospace;color:var(--gray-500);font-size:12px;width:20px;text-align:center;}
|
|
|
|
.regex-error{font-size:10.5px;color:var(--red-600);margin-top:2px;}
|
|
|
|
.test-panel{margin-top:18px;background:var(--gray-50);border:1px solid var(--gray-200);border-radius:8px;padding:14px 16px;}
|
|
.test-panel h4{margin:0 0 10px;font-size:13px;}
|
|
.test-row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;}
|
|
.test-row input{flex:1;min-width:220px;padding:9px 12px;border:1px solid var(--gray-300);border-radius:6px;font-size:13px;font-family:Consolas,monospace;}
|
|
.test-result{margin-top:12px;font-size:12.5px;padding:10px 12px;border-radius:8px;line-height:1.6;}
|
|
.test-result.hit{background:var(--green-50);color:#166534;border:1px solid #bbf7d0;}
|
|
.test-result.miss{background:var(--red-50);color:#991b1b;border:1px solid #fecaca;}
|
|
.test-result mark{background:#fde68a;padding:0 2px;border-radius:3px;}
|
|
|
|
.note-box{display:flex;gap:10px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;padding:12px 14px;font-size:12.5px;color:#1e40af;line-height:1.6;}
|
|
.note-box .ico{font-size:16px;}
|
|
|
|
.spinner{width:42px;height:42px;border:4px solid var(--gray-200);border-top-color:var(--purple-600);border-radius:50%;margin:18px auto 14px;animation:spin .8s linear infinite;}
|
|
@keyframes spin{to{transform:rotate(360deg);}}
|
|
.check-circle{width:52px;height:52px;border-radius:50%;background:var(--green-50);color:var(--green-600);display:flex;align-items:center;justify-content:center;margin:14px auto 16px;font-size:28px;}
|
|
.modal-log{text-align:left;background:var(--gray-50);border-radius:8px;padding:12px 14px;margin:14px 0;font-size:12px;font-family:Consolas,monospace;color:var(--gray-600);max-height:200px;overflow-y:auto;}
|
|
.modal-log div{padding:2px 0;}
|
|
.modal-close{margin-top:10px;width:100%;}
|
|
|
|
.toast{position:fixed;bottom:24px;right:24px;background:var(--gray-800);color:#fff;padding:12px 18px;border-radius:8px;font-size:13.5px;box-shadow:0 8px 20px rgba(0,0,0,.25);display:none;z-index:60;align-items:center;gap:8px;}
|
|
.toast.show{display:flex;}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="sidebar">
|
|
<div class="sidebar-header">MES系統 <span class="ver">v1.2.1</span></div>
|
|
<div class="nav-item">首頁儀表板</div>
|
|
<div class="nav-item">部門管理</div>
|
|
<div class="nav-item">人員/權限管理</div>
|
|
<div class="nav-item">物料管理</div>
|
|
<div class="nav-item">設備管理</div>
|
|
<div class="nav-item">設備維修管理</div>
|
|
<div class="nav-item active">設備加工檔</div>
|
|
<div class="nav-sub current">雷刻檔設定(.txt)</div>
|
|
<div class="nav-sub">加工參數檔設定(.xml)</div>
|
|
<div class="nav-sub">加工參數檔設定(.dat)</div>
|
|
<div class="nav-item">品質管理</div>
|
|
<div class="nav-item">異常管理</div>
|
|
<div class="nav-item">課程模組</div>
|
|
<div class="nav-item">製造執行中心</div>
|
|
<div class="nav-sub">工單管理</div>
|
|
<div class="nav-sub">工單生產紀錄查詢</div>
|
|
<div class="nav-sub">生產即時監控看板</div>
|
|
<div class="nav-sub">工單列印設定</div>
|
|
<div class="nav-item">刀具管理</div>
|
|
<div class="sidebar-footer">
|
|
<div class="avatar">👤</div>
|
|
<div>admin<br><span style="opacity:.6;font-size:11px;">admin</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main">
|
|
<div class="topbar">
|
|
雷刻檔設定
|
|
<div class="breadcrumb">設備加工檔 / 雷刻檔設定(.txt)</div>
|
|
</div>
|
|
|
|
<div class="content">
|
|
|
|
<div class="card">
|
|
<div class="card-head">
|
|
<div>
|
|
<h2>類別 ↔ 正規表示式規則綁定</h2>
|
|
<p>目前依「產品型態」欄位分為 6 種類別,可視需要自行新增類別,各類別以正規表示式(Regular Expression)規則定義比對與轉換邏輯,由上而下依序比對,第一個命中的規則生效</p>
|
|
</div>
|
|
<div style="display:flex;gap:8px;">
|
|
<button class="btn btn-outline btn-sm" id="addCatBtn">+ 新增類別</button>
|
|
<button class="btn btn-blue btn-sm" id="editModeBtn">✎ 修改</button>
|
|
<button class="btn btn-gray btn-sm" id="cancelEditBtn" style="display:none;">✕ 取消</button>
|
|
<button class="btn btn-green btn-sm" id="saveAllBtn" style="display:none;">儲存全部設定</button>
|
|
</div>
|
|
</div>
|
|
<div class="note-box" id="lockedHint" style="margin:0 20px;">
|
|
<span class="ico">🔒</span>
|
|
<div>目前為檢視模式,備註/狀態/規則/FTPS 設定皆不可編輯,「儲存」按鈕也會暫時隱藏。請點「✎ 修改」解鎖後即可編輯,並會出現「儲存全部設定」按鈕。</div>
|
|
</div>
|
|
|
|
<div style="overflow-x:auto;">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>類別</th>
|
|
<th>比對規則 (Regular Expression)</th>
|
|
<th>備註</th>
|
|
<th>FTPS 上傳設定</th>
|
|
<th>狀態</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="bindingBody"></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card-foot">
|
|
<span class="last-saved" id="lastSaved">上次儲存:2026/08/09 上午 10:05</span>
|
|
<span class="last-saved">共 <span id="bindingCount">4</span> 筆類別綁定</span>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add category modal -->
|
|
<div class="modal-overlay" id="addCatModal">
|
|
<div class="modal">
|
|
<h3>新增類別</h3>
|
|
<p>建立一個全新的類別,內容全空,可自行編輯正規表示式規則與 FTPS 設定後儲存</p>
|
|
<div style="text-align:left;margin-top:16px;display:flex;flex-direction:column;gap:14px;">
|
|
<div class="field">
|
|
<label>類別名稱</label>
|
|
<input type="text" id="newCatName" placeholder="例如 RGP外銷">
|
|
</div>
|
|
<div class="field">
|
|
<label>備註(選填)</label>
|
|
<input type="text" id="newCatDesc" placeholder="例如 說明此類別用途">
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;gap:10px;margin-top:20px;">
|
|
<button class="btn btn-gray" style="flex:1;" id="cancelAddCatBtn">取消</button>
|
|
<button class="btn btn-green" style="flex:1;" id="confirmAddCatBtn">建立</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Confirm modal -->
|
|
<div class="modal-overlay" id="confirmModal">
|
|
<div class="modal">
|
|
<h3 id="confirmTitle">確認刪除</h3>
|
|
<p id="confirmDesc"></p>
|
|
<div style="display:flex;gap:10px;margin-top:20px;">
|
|
<button class="btn btn-gray" style="flex:1;" id="confirmCancelBtn">取消</button>
|
|
<button class="btn" style="flex:1;background:var(--red-600);color:#fff;" id="confirmOkBtn">刪除</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Regex rule editor modal -->
|
|
<div class="modal-overlay" id="ruleModal">
|
|
<div class="modal rule-modal">
|
|
<div class="rule-modal-head">
|
|
<div>
|
|
<h3 id="ruleModalTitle">編輯正規表示式規則</h3>
|
|
<p id="ruleModalSub"></p>
|
|
</div>
|
|
<button class="icon-btn" id="closeRuleModalBtn">✕ 關閉</button>
|
|
</div>
|
|
<div class="rule-modal-body">
|
|
<div class="note-box" style="margin-bottom:14px;">
|
|
<span class="ico">ℹ️</span>
|
|
<div>規則由上而下依序比對,第一個 Pattern 命中即套用其「轉換結果」,可用 <code>()</code> 建立擷取群組,並在轉換結果欄用 <code>$1</code>、<code>$2</code> 帶入擷取內容。</div>
|
|
</div>
|
|
|
|
<table class="rule-table">
|
|
<thead>
|
|
<tr>
|
|
<th>順序</th>
|
|
<th style="min-width:220px;">Regular Expression Pattern</th>
|
|
<th style="min-width:140px;">轉換結果</th>
|
|
<th style="min-width:160px;">說明</th>
|
|
<th style="width:56px;">啟用</th>
|
|
<th style="width:110px;">排序</th>
|
|
<th style="width:36px;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="ruleTableBody"></tbody>
|
|
</table>
|
|
|
|
<button class="btn btn-outline btn-sm" id="addRuleBtn" style="margin-top:12px;">+ 新增規則</button>
|
|
|
|
<div class="test-panel">
|
|
<h4>🧪 測試比對</h4>
|
|
<p style="margin:0 0 10px;font-size:11.5px;color:var(--gray-500);">比對字串組成規則:<b>物料代碼第 4、5 碼(物料設計碼)</b> + <b>客戶代碼</b> + <b>物料代碼第 3 碼</b></p>
|
|
<div class="test-row">
|
|
<input type="text" id="testMaterialCode" placeholder="物料代碼(例如 FHKCZAB123)">
|
|
<input type="text" id="testCustomerCode" placeholder="客戶代碼(例如 71)">
|
|
<button class="btn btn-purple btn-sm" id="runTestBtn">執行測試</button>
|
|
</div>
|
|
<div id="composedPreview" style="margin-top:8px;font-size:12px;color:var(--gray-500);font-family:Consolas,monospace;"></div>
|
|
<div id="testResult"></div>
|
|
</div>
|
|
</div>
|
|
<div class="rule-modal-foot">
|
|
<span style="font-size:12px;color:var(--gray-500);" id="ruleCountLabel"></span>
|
|
<div style="display:flex;gap:8px;">
|
|
<button class="btn btn-gray btn-sm" id="cancelRuleBtn">取消</button>
|
|
<button class="btn btn-green btn-sm" id="applyRuleBtn">套用規則</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- FTPS settings modal -->
|
|
<div class="modal-overlay" id="ftpsModal">
|
|
<div class="modal wide">
|
|
<h3 id="ftpsModalTitle">FTPS 連線設定</h3>
|
|
<p id="ftpsModalSub"></p>
|
|
<div class="form-grid" style="text-align:left;margin-top:16px;">
|
|
<div class="field">
|
|
<label>Host</label>
|
|
<input type="text" id="ftpsHost" placeholder="ftps.example.com">
|
|
</div>
|
|
<div class="field">
|
|
<label>Port</label>
|
|
<input type="text" id="ftpsPort" placeholder="21">
|
|
</div>
|
|
<div class="field">
|
|
<label>帳號</label>
|
|
<input type="text" id="ftpsUser" placeholder="帳號">
|
|
</div>
|
|
<div class="field">
|
|
<label>密碼</label>
|
|
<div class="pw-wrap">
|
|
<input type="password" id="ftpsPass" placeholder="密碼">
|
|
<button type="button" id="toggleFtpsPw">顯示</button>
|
|
</div>
|
|
</div>
|
|
<div class="field full">
|
|
<label>遠端路徑</label>
|
|
<input type="text" id="ftpsPath" placeholder="/txt/一般片/">
|
|
</div>
|
|
<div class="field">
|
|
<label>傳輸模式</label>
|
|
<select id="ftpsMode">
|
|
<option value="explicit">明文 FTPS(Explicit)</option>
|
|
<option value="implicit">隱式 FTPS(Implicit)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;gap:10px;margin-top:20px;">
|
|
<button class="btn btn-outline" style="flex:1;" id="testFtpsBtn">🔌 測試連線</button>
|
|
<button class="btn btn-gray" style="flex:1;" id="cancelFtpsBtn">取消</button>
|
|
<button class="btn btn-green" style="flex:1;" id="saveFtpsBtn">儲存</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Save action modal -->
|
|
<div class="modal-overlay" id="actionModal">
|
|
<div class="modal">
|
|
<div id="modalLoading">
|
|
<h3 id="loadingTitle">處理中</h3>
|
|
<p id="loadingDesc">請稍候…</p>
|
|
<div class="spinner"></div>
|
|
<div class="modal-log" id="loadingLog"></div>
|
|
</div>
|
|
<div id="modalResult" style="display:none;">
|
|
<div class="check-circle">✓</div>
|
|
<h3 id="resultTitle"></h3>
|
|
<p id="resultDesc"></p>
|
|
<div class="modal-log" id="resultLog"></div>
|
|
<button class="btn btn-blue modal-close" id="modalCloseBtn">關閉</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast" id="toast"></div>
|
|
|
|
<script>
|
|
// ---- 6 種類別(依「產品型態」欄位),各自綁定一組正規表示式規則(取代原本的 .flow 檔案) ----
|
|
let bindings = [
|
|
{
|
|
cat:"RGP",
|
|
tagClass:"tag-rgp",
|
|
enabled:true,
|
|
desc:"1:RGP 一般片",
|
|
ftps:{ host:"ftps.txtsvr.local", port:"21", user:"ftp_rgp", pass:"RgpPass!234", path:"/txt/RGP/", mode:"explicit" },
|
|
rules:[
|
|
{ pattern:"^(FK|Target|BC|CYL|DIA)$", output:"值 × 100", desc:"數值統一轉換為刻字格式", enabled:true },
|
|
{ pattern:".*", output:"C/CT", desc:"其餘 → C/CT(預設兜底規則)", enabled:true }
|
|
]
|
|
},
|
|
{
|
|
cat:"OK",
|
|
tagClass:"tag-ok",
|
|
enabled:true,
|
|
desc:"2:OK 一般片",
|
|
ftps:{ host:"ftps.txtsvr.local", port:"21", user:"ftp_ok", pass:"OkPass!234", path:"/txt/OK/", mode:"explicit" },
|
|
rules:[
|
|
{ pattern:"HiLine", output:"H/HT", desc:"HiLine → H/HT", enabled:true },
|
|
{ pattern:"myOK\\s*Classic", output:"C/CT", desc:"myOK Classic → C/CT", enabled:true },
|
|
{ pattern:"myOK\\s*Pro", output:"P/PT", desc:"myOK Pro → P/PT", enabled:true },
|
|
{ pattern:".*", output:"C/CT", desc:"其餘 → C/CT(預設兜底規則)", enabled:true }
|
|
]
|
|
},
|
|
{
|
|
cat:"RGP試片",
|
|
tagClass:"tag-rgptest",
|
|
enabled:true,
|
|
desc:"3:RGP 試片專用",
|
|
ftps:{ host:"", port:"21", user:"", pass:"", path:"", mode:"explicit" },
|
|
rules:[
|
|
{ pattern:".*", output:"上刻=流水序號/下刻=RGP", desc:"固定刻字規則,不比對代碼", enabled:true }
|
|
]
|
|
},
|
|
{
|
|
cat:"OK試片",
|
|
tagClass:"tag-oktest",
|
|
enabled:true,
|
|
desc:"4:OK 試片專用",
|
|
ftps:{ host:"", port:"21", user:"", pass:"", path:"", mode:"explicit" },
|
|
rules:[
|
|
{ pattern:".*", output:"上刻=流水序號/下刻=OK", desc:"固定刻字規則,不比對代碼", enabled:true }
|
|
]
|
|
},
|
|
{
|
|
cat:"AOK",
|
|
tagClass:"tag-aok",
|
|
enabled:false,
|
|
desc:"5:AOK 一般片",
|
|
ftps:{ host:"", port:"21", user:"", pass:"", path:"", mode:"explicit" },
|
|
rules:[
|
|
{ pattern:".*", output:"C/CT", desc:"規則待確認(預設兜底規則)", enabled:true }
|
|
]
|
|
},
|
|
{
|
|
cat:"AOK試片",
|
|
tagClass:"tag-aoktest",
|
|
enabled:false,
|
|
desc:"6:AOK 試片專用",
|
|
ftps:{ host:"", port:"21", user:"", pass:"", path:"", mode:"explicit" },
|
|
rules:[
|
|
{ pattern:".*", output:"上刻=流水序號/下刻=AOK", desc:"固定刻字規則,不比對代碼", enabled:true }
|
|
]
|
|
}
|
|
];
|
|
|
|
let editingIdx = null; // 目前正在編輯規則的 binding index
|
|
let editingRules = []; // 編輯中的暫存規則陣列(取消時可捨棄)
|
|
|
|
// ---- 編輯鎖定狀態:預設檢視(唯讀),點「修改」才能編輯,儲存或取消後鎖回 ----
|
|
let editMode = false;
|
|
let editSnapshot = null; // 進入編輯模式時,bindings 的深層快照,供「取消」還原
|
|
|
|
function updateEditModeUI(){
|
|
document.getElementById("editModeBtn").style.display = editMode ? "none" : "inline-flex";
|
|
document.getElementById("cancelEditBtn").style.display = editMode ? "inline-flex" : "none";
|
|
document.getElementById("saveAllBtn").style.display = editMode ? "inline-flex" : "none";
|
|
document.getElementById("lockedHint").style.display = editMode ? "none" : "flex";
|
|
}
|
|
|
|
function renderBindings(){
|
|
const body = document.getElementById("bindingBody");
|
|
body.innerHTML = "";
|
|
const lockAttr = editMode ? "" : "disabled";
|
|
bindings.forEach((b,idx)=>{
|
|
const tr = document.createElement("tr");
|
|
const enabledCount = b.rules.filter(r=>r.enabled).length;
|
|
const firstPattern = b.rules[0] ? b.rules[0].pattern : "";
|
|
tr.innerHTML = `
|
|
<td><span class="tag ${b.tagClass}">${b.cat}</span></td>
|
|
<td>
|
|
<button class="rule-summary-btn" data-idx="${idx}" data-action="edit-rules" ${lockAttr}>
|
|
<span class="count-row">📐 編輯規則 <span class="count-badge">${b.rules.length} 條</span></span>
|
|
<span class="preview-pattern">${b.rules.length===0 ? "尚未設定規則,請點擊編輯" : `/${escapeHtml(firstPattern)}/ ${b.rules.length>1?`… 等 ${enabledCount} 條啟用中`:""}`}</span>
|
|
</button>
|
|
</td>
|
|
<td><input type="text" class="remark-input" data-idx="${idx}" data-role="desc" value="${escapeHtml(b.desc)}" ${lockAttr}></td>
|
|
<td>
|
|
<button class="ftps-btn" data-idx="${idx}" data-action="edit-ftps" ${lockAttr}>
|
|
<span class="ftps-title">🔌 FTPS 設定</span>
|
|
<span class="ftps-dest ${ftpsIsSet(b)?"":"unset"}">${escapeHtml(ftpsDestString(b) || "未設定")}</span>
|
|
</button>
|
|
</td>
|
|
<td>
|
|
<label class="toggle">
|
|
<input type="checkbox" data-idx="${idx}" data-role="enabled" ${b.enabled?"checked":""} ${lockAttr}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</td>
|
|
<td>
|
|
<div class="row-actions">
|
|
<button class="icon-btn" data-idx="${idx}" data-action="test-quick" ${lockAttr}>▶ 快速測試</button>
|
|
<button class="icon-btn danger" data-idx="${idx}" data-action="delete-cat" ${lockAttr} title="刪除此類別">🗑 刪除</button>
|
|
</div>
|
|
</td>
|
|
`;
|
|
body.appendChild(tr);
|
|
});
|
|
document.getElementById("bindingCount").textContent = bindings.length;
|
|
updateEditModeUI();
|
|
}
|
|
|
|
// ---- 新增類別(全空,可自行編輯規則/FTPS 後儲存) ----
|
|
const addCatModal = document.getElementById("addCatModal");
|
|
document.getElementById("addCatBtn").addEventListener("click", ()=>{
|
|
if(editMode){
|
|
showToast("請先儲存或取消目前的編輯,才能新增類別");
|
|
return;
|
|
}
|
|
document.getElementById("newCatName").value = "";
|
|
document.getElementById("newCatDesc").value = "";
|
|
addCatModal.classList.add("show");
|
|
setTimeout(()=> document.getElementById("newCatName").focus(), 50);
|
|
});
|
|
document.getElementById("cancelAddCatBtn").addEventListener("click", ()=> addCatModal.classList.remove("show"));
|
|
document.getElementById("confirmAddCatBtn").addEventListener("click", ()=>{
|
|
const cat = document.getElementById("newCatName").value.trim();
|
|
const desc = document.getElementById("newCatDesc").value.trim();
|
|
if(!cat){ showToast("請輸入類別名稱"); return; }
|
|
if(bindings.some(b=>b.cat===cat)){ showToast(`類別「${cat}」已存在`); return; }
|
|
|
|
editSnapshot = JSON.parse(JSON.stringify(bindings)); // 記錄新增前的狀態,供「取消」還原(會一併移除本次新增的類別)
|
|
bindings.push({
|
|
cat,
|
|
tagClass: "tag-custom",
|
|
enabled: true,
|
|
desc,
|
|
ftps: { host:"", port:"21", user:"", pass:"", path:"", mode:"explicit" },
|
|
rules: []
|
|
});
|
|
editMode = true;
|
|
addCatModal.classList.remove("show");
|
|
renderBindings();
|
|
showToast(`已新增類別「${cat}」,目前已進入編輯模式,可編輯規則/FTPS 後儲存`);
|
|
});
|
|
|
|
function escapeHtml(s){
|
|
return (s||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
|
}
|
|
|
|
function ftpsIsSet(b){
|
|
const f = b.ftps;
|
|
return !!(f && f.host && f.user && f.path);
|
|
}
|
|
function ftpsDestString(b){
|
|
const f = b.ftps;
|
|
if(!ftpsIsSet(b)) return null;
|
|
return `${f.user}@${f.host}:${f.port||21}${f.path}`;
|
|
}
|
|
|
|
document.getElementById("editModeBtn").addEventListener("click", ()=>{
|
|
editMode = true;
|
|
editSnapshot = JSON.parse(JSON.stringify(bindings));
|
|
renderBindings();
|
|
});
|
|
|
|
document.getElementById("cancelEditBtn").addEventListener("click", ()=>{
|
|
if(editSnapshot){
|
|
bindings = editSnapshot;
|
|
}
|
|
editMode = false;
|
|
editSnapshot = null;
|
|
renderBindings();
|
|
showToast("已取消編輯,變更未儲存");
|
|
});
|
|
|
|
document.getElementById("bindingBody").addEventListener("change", e=>{
|
|
if(!editMode) return;
|
|
const idx = e.target.dataset.idx;
|
|
const role = e.target.dataset.role;
|
|
if(idx===undefined) return;
|
|
if(role==="desc") bindings[idx].desc = e.target.value;
|
|
if(role==="enabled") bindings[idx].enabled = e.target.checked;
|
|
});
|
|
|
|
document.getElementById("bindingBody").addEventListener("click", e=>{
|
|
const btn = e.target.closest("button[data-action]");
|
|
if(!btn) return;
|
|
const idx = Number(btn.dataset.idx);
|
|
const action = btn.dataset.action;
|
|
if(action==="edit-rules") openRuleModal(idx);
|
|
if(action==="test-quick") openRuleModal(idx, true);
|
|
if(action==="edit-ftps") openFtpsModal(idx);
|
|
if(action==="delete-cat") confirmDeleteCategory(idx);
|
|
});
|
|
|
|
// ---- 通用確認刪除 modal ----
|
|
const confirmModal = document.getElementById("confirmModal");
|
|
let confirmCallback = null;
|
|
function openConfirm(title, desc, onConfirm){
|
|
document.getElementById("confirmTitle").textContent = title;
|
|
document.getElementById("confirmDesc").textContent = desc;
|
|
confirmCallback = onConfirm;
|
|
confirmModal.classList.add("show");
|
|
}
|
|
document.getElementById("confirmCancelBtn").addEventListener("click", ()=>{
|
|
confirmModal.classList.remove("show");
|
|
confirmCallback = null;
|
|
});
|
|
document.getElementById("confirmOkBtn").addEventListener("click", ()=>{
|
|
confirmModal.classList.remove("show");
|
|
if(confirmCallback) confirmCallback();
|
|
confirmCallback = null;
|
|
});
|
|
|
|
// ---- 刪除整個類別(例如設定有誤時,可整份刪除重建)----
|
|
function confirmDeleteCategory(idx){
|
|
if(!editMode) return;
|
|
if(bindings.length<=1){
|
|
showToast("至少需保留一個類別,無法刪除");
|
|
return;
|
|
}
|
|
const b = bindings[idx];
|
|
openConfirm(
|
|
`刪除類別「${b.cat}」?`,
|
|
`即將刪除「${b.cat}」的正規表示式規則與 FTPS 設定,此動作無法復原。`,
|
|
()=>{
|
|
bindings.splice(idx,1);
|
|
renderBindings();
|
|
showToast(`已刪除「${b.cat}」,別忘了點「儲存全部設定」`);
|
|
}
|
|
);
|
|
}
|
|
|
|
// ---------- FTPS settings modal ----------
|
|
const ftpsModal = document.getElementById("ftpsModal");
|
|
let ftpsEditingIdx = null;
|
|
|
|
function openFtpsModal(idx){
|
|
ftpsEditingIdx = idx;
|
|
const b = bindings[idx];
|
|
const f = b.ftps;
|
|
document.getElementById("ftpsModalTitle").textContent = `FTPS 連線設定 — ${b.cat}`;
|
|
document.getElementById("ftpsModalSub").textContent = `此設定僅套用於「${b.cat}」產出的 .txt 檔案上傳`;
|
|
document.getElementById("ftpsHost").value = f.host;
|
|
document.getElementById("ftpsPort").value = f.port;
|
|
document.getElementById("ftpsUser").value = f.user;
|
|
document.getElementById("ftpsPass").value = f.pass;
|
|
document.getElementById("ftpsPath").value = f.path;
|
|
document.getElementById("ftpsMode").value = f.mode;
|
|
document.getElementById("ftpsPass").type = "password";
|
|
document.getElementById("toggleFtpsPw").textContent = "顯示";
|
|
ftpsModal.classList.add("show");
|
|
}
|
|
document.getElementById("cancelFtpsBtn").addEventListener("click", ()=> ftpsModal.classList.remove("show"));
|
|
document.getElementById("toggleFtpsPw").addEventListener("click", (e)=>{
|
|
const input = document.getElementById("ftpsPass");
|
|
if(input.type==="password"){ input.type="text"; e.target.textContent="隱藏"; }
|
|
else { input.type="password"; e.target.textContent="顯示"; }
|
|
});
|
|
|
|
document.getElementById("saveFtpsBtn").addEventListener("click", ()=>{
|
|
bindings[ftpsEditingIdx].ftps = {
|
|
host: document.getElementById("ftpsHost").value.trim(),
|
|
port: document.getElementById("ftpsPort").value.trim() || "21",
|
|
user: document.getElementById("ftpsUser").value.trim(),
|
|
pass: document.getElementById("ftpsPass").value,
|
|
path: document.getElementById("ftpsPath").value.trim(),
|
|
mode: document.getElementById("ftpsMode").value
|
|
};
|
|
ftpsModal.classList.remove("show");
|
|
renderBindings();
|
|
showToast(`已更新「${bindings[ftpsEditingIdx].cat}」的 FTPS 設定,請記得儲存全部設定`);
|
|
});
|
|
|
|
document.getElementById("testFtpsBtn").addEventListener("click", ()=>{
|
|
const host = document.getElementById("ftpsHost").value.trim();
|
|
const user = document.getElementById("ftpsUser").value.trim();
|
|
const port = document.getElementById("ftpsPort").value.trim() || "21";
|
|
const path = document.getElementById("ftpsPath").value.trim();
|
|
const mode = document.getElementById("ftpsMode").value;
|
|
if(!host || !user){
|
|
showToast("請至少填寫 Host 與帳號再測試連線");
|
|
return;
|
|
}
|
|
runModalSequence({
|
|
title:"測試 FTPS 連線",
|
|
desc:`正在連線至 ${host}:${port}`,
|
|
steps:[
|
|
`建立 ${mode==="implicit"?"隱式":"明文"} FTPS 連線...`,
|
|
`以帳號「${user}」登入...`,
|
|
`切換至路徑 ${path || "/"}...`,
|
|
"驗證讀寫權限..."
|
|
],
|
|
onDone: ()=> showModalResult({
|
|
title:"連線成功",
|
|
desc:`已成功連線並登入 ${host}`,
|
|
log:[`✓ TLS 交握成功`, `✓ 帳號驗證通過`, `✓ 路徑 ${path || "/"} 可讀寫`]
|
|
})
|
|
});
|
|
});
|
|
|
|
// ---------- Rule editor modal ----------
|
|
const ruleModal = document.getElementById("ruleModal");
|
|
|
|
function openRuleModal(idx, focusTest){
|
|
editingIdx = idx;
|
|
editingRules = JSON.parse(JSON.stringify(bindings[idx].rules));
|
|
const b = bindings[idx];
|
|
document.getElementById("ruleModalTitle").textContent = `編輯正規表示式規則 — ${b.cat}`;
|
|
document.getElementById("ruleModalSub").textContent = `${b.desc || ""}`;
|
|
document.getElementById("testMaterialCode").value = "";
|
|
document.getElementById("testCustomerCode").value = "";
|
|
document.getElementById("testResult").innerHTML = "";
|
|
document.getElementById("composedPreview").innerHTML = "";
|
|
renderRuleTable();
|
|
ruleModal.classList.add("show");
|
|
if(focusTest){
|
|
setTimeout(()=> document.getElementById("testMaterialCode").focus(), 50);
|
|
}
|
|
}
|
|
|
|
function isValidRegex(pattern){
|
|
try{ new RegExp(pattern); return true; } catch(e){ return false; }
|
|
}
|
|
|
|
function renderRuleTable(){
|
|
const tbody = document.getElementById("ruleTableBody");
|
|
tbody.innerHTML = "";
|
|
editingRules.forEach((r, i)=>{
|
|
const valid = isValidRegex(r.pattern);
|
|
const tr = document.createElement("tr");
|
|
tr.innerHTML = `
|
|
<td class="rule-order">${i+1}</td>
|
|
<td>
|
|
<input type="text" class="rule-input wide ${valid?"":"invalid"}" data-i="${i}" data-role="pattern" value="${escapeHtml(r.pattern)}" placeholder="例如 ^FR..01$">
|
|
${valid?"":'<div class="regex-error">⚠ 無效的正規表示式</div>'}
|
|
</td>
|
|
<td><input type="text" class="rule-input" data-i="${i}" data-role="output" value="${escapeHtml(r.output)}" placeholder="轉換結果 / $1"></td>
|
|
<td><input type="text" class="rule-input" data-i="${i}" data-role="desc" value="${escapeHtml(r.desc)}" placeholder="說明(選填)"></td>
|
|
<td style="text-align:center;">
|
|
<label class="toggle">
|
|
<input type="checkbox" data-i="${i}" data-role="enabled" ${r.enabled?"checked":""}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</td>
|
|
<td>
|
|
<div class="row-actions">
|
|
<button class="icon-btn" data-i="${i}" data-action="move-up" ${i===0?"disabled":""} title="上移">↑</button>
|
|
<button class="icon-btn" data-i="${i}" data-action="move-down" ${i===editingRules.length-1?"disabled":""} title="下移">↓</button>
|
|
</div>
|
|
</td>
|
|
<td><button class="icon-btn danger" data-i="${i}" data-action="delete-rule" title="刪除">🗑</button></td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
document.getElementById("ruleCountLabel").textContent = `共 ${editingRules.length} 條規則`;
|
|
}
|
|
|
|
document.getElementById("ruleTableBody").addEventListener("input", e=>{
|
|
const role = e.target.dataset.role;
|
|
const i = e.target.dataset.i;
|
|
if(i===undefined || !role) return;
|
|
editingRules[i][role] = e.target.value;
|
|
if(role==="pattern"){
|
|
const valid = isValidRegex(e.target.value);
|
|
e.target.classList.toggle("invalid", !valid);
|
|
const errDiv = e.target.parentElement.querySelector(".regex-error");
|
|
if(!valid && !errDiv){
|
|
const div = document.createElement("div");
|
|
div.className = "regex-error";
|
|
div.textContent = "⚠ 無效的正規表示式";
|
|
e.target.parentElement.appendChild(div);
|
|
} else if(valid && errDiv){
|
|
errDiv.remove();
|
|
}
|
|
}
|
|
});
|
|
|
|
document.getElementById("ruleTableBody").addEventListener("change", e=>{
|
|
const role = e.target.dataset.role;
|
|
const i = e.target.dataset.i;
|
|
if(role==="enabled"){
|
|
editingRules[i].enabled = e.target.checked;
|
|
}
|
|
});
|
|
|
|
document.getElementById("ruleTableBody").addEventListener("click", e=>{
|
|
const btn = e.target.closest("button[data-action]");
|
|
if(!btn) return;
|
|
const i = Number(btn.dataset.i);
|
|
const action = btn.dataset.action;
|
|
if(action==="delete-rule"){
|
|
editingRules.splice(i,1);
|
|
renderRuleTable();
|
|
}
|
|
if(action==="move-up" && i>0){
|
|
[editingRules[i-1], editingRules[i]] = [editingRules[i], editingRules[i-1]];
|
|
renderRuleTable();
|
|
}
|
|
if(action==="move-down" && i<editingRules.length-1){
|
|
[editingRules[i+1], editingRules[i]] = [editingRules[i], editingRules[i+1]];
|
|
renderRuleTable();
|
|
}
|
|
});
|
|
|
|
document.getElementById("addRuleBtn").addEventListener("click", ()=>{
|
|
editingRules.push({ pattern:"", output:"", desc:"", enabled:true });
|
|
renderRuleTable();
|
|
const inputs = document.querySelectorAll('#ruleTableBody input[data-role="pattern"]');
|
|
const last = inputs[inputs.length-1];
|
|
if(last) last.focus();
|
|
});
|
|
|
|
document.getElementById("closeRuleModalBtn").addEventListener("click", ()=> ruleModal.classList.remove("show"));
|
|
document.getElementById("cancelRuleBtn").addEventListener("click", ()=> ruleModal.classList.remove("show"));
|
|
|
|
document.getElementById("applyRuleBtn").addEventListener("click", ()=>{
|
|
const hasInvalid = editingRules.some(r=> r.pattern.trim()!=="" && !isValidRegex(r.pattern));
|
|
if(hasInvalid){
|
|
showToast("有規則的正規表示式格式錯誤,請修正後再套用");
|
|
return;
|
|
}
|
|
bindings[editingIdx].rules = JSON.parse(JSON.stringify(editingRules));
|
|
renderBindings();
|
|
ruleModal.classList.remove("show");
|
|
showToast(`已更新「${bindings[editingIdx].cat}」的規則,請記得儲存全部設定`);
|
|
});
|
|
|
|
// ---------- Test matching engine ----------
|
|
function runRuleTest(rules, testStr){
|
|
for(const r of rules){
|
|
if(!r.enabled) continue;
|
|
if(!isValidRegex(r.pattern)) continue;
|
|
const re = new RegExp(r.pattern);
|
|
const m = testStr.match(re);
|
|
if(m){
|
|
return { rule:r, match:m };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// 比對字串組成規則:物料代碼第 4、5 碼(物料設計碼)+ 客戶代碼 + 物料代碼第 3 碼
|
|
function composeTestString(){
|
|
const material = document.getElementById("testMaterialCode").value || "";
|
|
const customer = document.getElementById("testCustomerCode").value || "";
|
|
const designCode = material.substring(3,5); // 第 4、5 碼
|
|
const materialCh3 = material.substring(2,3); // 第 3 碼
|
|
return { material, customer, designCode, materialCh3, composed: designCode + customer + materialCh3 };
|
|
}
|
|
|
|
function updateComposedPreview(){
|
|
const { material, customer, designCode, materialCh3, composed } = composeTestString();
|
|
const box = document.getElementById("composedPreview");
|
|
if(material.trim()===""){
|
|
box.innerHTML = "";
|
|
return;
|
|
}
|
|
if(material.length < 5){
|
|
box.innerHTML = `⚠ 物料代碼長度不足 5 碼,無法取出第 4、5 碼`;
|
|
return;
|
|
}
|
|
box.innerHTML = `組合後比對字串:<b>${escapeHtml(composed)}</b> (物料設計碼「${escapeHtml(designCode)}」+ 客戶代碼「${escapeHtml(customer)}」+ 物料第3碼「${escapeHtml(materialCh3)}」)`;
|
|
}
|
|
document.getElementById("testMaterialCode").addEventListener("input", updateComposedPreview);
|
|
document.getElementById("testCustomerCode").addEventListener("input", updateComposedPreview);
|
|
|
|
document.getElementById("runTestBtn").addEventListener("click", ()=>{
|
|
const resultBox = document.getElementById("testResult");
|
|
const { material, composed } = composeTestString();
|
|
if(material.trim()===""){
|
|
resultBox.innerHTML = `<div class="test-result miss">請輸入物料代碼</div>`;
|
|
return;
|
|
}
|
|
if(material.length < 5){
|
|
resultBox.innerHTML = `<div class="test-result miss">物料代碼長度不足 5 碼,無法取出第 4、5 碼,請確認輸入</div>`;
|
|
return;
|
|
}
|
|
const result = runRuleTest(editingRules, composed);
|
|
if(result){
|
|
const idx = editingRules.indexOf(result.rule);
|
|
const highlighted = composed.replace(result.match[0], `<mark>${escapeHtml(result.match[0])}</mark>`);
|
|
let outputText = result.rule.output;
|
|
result.match.forEach((g,i)=>{ if(i>0 && g!==undefined) outputText = outputText.replace(new RegExp("\\$"+i,"g"), g); });
|
|
resultBox.innerHTML = `<div class="test-result hit">
|
|
✓ 命中規則 #${idx+1}:<code>/${escapeHtml(result.rule.pattern)}/</code><br>
|
|
比對字串(物料設計碼+客戶代碼+物料第3碼):${highlighted}<br>
|
|
轉換結果:<b>${escapeHtml(outputText)}</b>${result.rule.desc?`(${escapeHtml(result.rule.desc)})`:""}
|
|
</div>`;
|
|
} else {
|
|
resultBox.innerHTML = `<div class="test-result miss">✕ 沒有規則命中「${escapeHtml(composed)}」,請檢查規則或新增兜底規則(如 <code>.*</code>)</div>`;
|
|
}
|
|
});
|
|
document.getElementById("testMaterialCode").addEventListener("keydown", e=>{
|
|
if(e.key==="Enter") document.getElementById("runTestBtn").click();
|
|
});
|
|
document.getElementById("testCustomerCode").addEventListener("keydown", e=>{
|
|
if(e.key==="Enter") document.getElementById("runTestBtn").click();
|
|
});
|
|
|
|
// ---------- Save all ----------
|
|
const actionModal = document.getElementById("actionModal");
|
|
const modalLoading = document.getElementById("modalLoading");
|
|
const modalResult = document.getElementById("modalResult");
|
|
|
|
function runModalSequence({title, desc, steps, onDone}){
|
|
actionModal.classList.add("show");
|
|
modalLoading.style.display="block";
|
|
modalResult.style.display="none";
|
|
document.getElementById("loadingTitle").textContent = title;
|
|
document.getElementById("loadingDesc").textContent = desc;
|
|
const log = document.getElementById("loadingLog");
|
|
log.innerHTML = "";
|
|
let i=0;
|
|
const interval = setInterval(()=>{
|
|
if(i<steps.length){
|
|
const line = document.createElement("div");
|
|
line.textContent = "› " + steps[i];
|
|
log.appendChild(line);
|
|
log.scrollTop = log.scrollHeight;
|
|
i++;
|
|
} else {
|
|
clearInterval(interval);
|
|
setTimeout(onDone, 400);
|
|
}
|
|
}, 260);
|
|
}
|
|
function showModalResult({title, desc, log}){
|
|
modalLoading.style.display="none";
|
|
modalResult.style.display="block";
|
|
document.getElementById("resultTitle").textContent = title;
|
|
document.getElementById("resultDesc").textContent = desc;
|
|
document.getElementById("resultLog").innerHTML = (log||[]).map(l=>`<div>${l}</div>`).join("");
|
|
}
|
|
document.getElementById("modalCloseBtn").addEventListener("click", ()=> actionModal.classList.remove("show"));
|
|
|
|
document.getElementById("saveAllBtn").addEventListener("click", ()=>{
|
|
if(!editMode) return;
|
|
const totalRules = bindings.reduce((sum,b)=> sum + b.rules.length, 0);
|
|
const missingFtps = bindings.filter(b=> b.enabled && !ftpsIsSet(b));
|
|
if(missingFtps.length>0){
|
|
showToast(`「${missingFtps.map(b=>b.cat).join("、")}」尚未設定 FTPS,請先完成設定`);
|
|
return;
|
|
}
|
|
runModalSequence({
|
|
title:"儲存雷刻檔設定",
|
|
desc:"正在儲存類別 ↔ 正規表示式規則 ↔ FTPS 設定",
|
|
steps:[
|
|
"驗證所有正規表示式語法...",
|
|
`寫入 ${bindings.length} 個類別、共 ${totalRules} 條規則...`,
|
|
"驗證各類別 FTPS 連線設定...",
|
|
"更新產製 .txt 比對引擎與上傳設定..."
|
|
],
|
|
onDone: ()=>{
|
|
showModalResult({
|
|
title:"設定已儲存",
|
|
desc:`共 ${bindings.length} 個類別、${totalRules} 條規則已生效`,
|
|
log: bindings.map(b=>`✓ ${b.cat} → ${b.rules.length} 條規則,FTPS → ${ftpsDestString(b) || "未設定"}${b.enabled?"":"(類別停用)"}`)
|
|
});
|
|
document.getElementById("lastSaved").textContent = "上次儲存:" + new Date().toLocaleString('zh-TW',{hour12:true});
|
|
editMode = false;
|
|
editSnapshot = null;
|
|
renderBindings();
|
|
}
|
|
});
|
|
});
|
|
|
|
function showToast(msg){
|
|
const t = document.getElementById("toast");
|
|
t.textContent = "✓ " + msg;
|
|
t.classList.add("show");
|
|
setTimeout(()=>t.classList.remove("show"), 3000);
|
|
}
|
|
|
|
renderBindings();
|
|
</script>
|
|
</body>
|
|
</html>
|