|
<!DOCTYPE html>
|
|
<html lang="zh-Hant">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>MES系統 - 加工參數檔設定(.dat)</title>
|
|
<style>
|
|
:root{
|
|
--blue-900:#1e3a8a; --blue-800:#1e40af; --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;
|
|
--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:16px;}
|
|
|
|
.cat-tabs{display:flex;gap:8px;flex-wrap:wrap;}
|
|
.cat-tab{
|
|
padding:9px 16px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;
|
|
background:#fff;border:1px solid var(--gray-300);color:var(--gray-600);
|
|
display:flex;align-items:center;gap:8px;
|
|
}
|
|
.cat-tab .count{background:var(--gray-100);color:var(--gray-500);border-radius:999px;padding:1px 8px;font-size:11.5px;font-weight:700;}
|
|
.cat-tab.active{background:var(--blue-600);border-color:var(--blue-600);color:#fff;}
|
|
.cat-tab.active .count{background:rgba(255,255,255,.25);color:#fff;}
|
|
.cat-tab .fname{font-family:Consolas,monospace;font-weight:400;opacity:.8;font-size:11.5px;}
|
|
.cat-tab.add-tab{border-style:dashed;color:var(--blue-600);font-weight:700;}
|
|
.cat-tab.add-tab:hover{background:#eff6ff;border-color:var(--blue-500);}
|
|
|
|
.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;flex-wrap:wrap;}
|
|
.card-head h2{margin:0;font-size:15.5px;display:flex;align-items:center;gap:8px;}
|
|
.card-head p{margin:3px 0 0;font-size:12.5px;color:var(--gray-500);font-weight:400;}
|
|
.card-head .actions{display:flex;gap:8px;flex-wrap:wrap;}
|
|
|
|
.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-danger-outline{background:#fff;color:var(--red-600);border:1px solid #fecaca;}
|
|
.btn-sm{padding:6px 12px;font-size:12.5px;}
|
|
.btn-xs{padding:4px 9px;font-size:11.5px;}
|
|
|
|
table{width:100%;border-collapse:collapse;min-width:1080px;}
|
|
thead th{text-align:left;font-size:12px;color:var(--gray-500);font-weight:600;padding:10px 10px;border-bottom:1px solid var(--gray-200);white-space:nowrap;background:var(--gray-50);position:sticky;top:0;}
|
|
tbody td{padding:6px 10px;border-bottom:1px solid var(--gray-100);font-size:13px;vertical-align:middle;}
|
|
tbody tr:hover{background:var(--gray-50);}
|
|
tbody tr.dup td{background:var(--red-50);}
|
|
|
|
.section-row td{
|
|
background:#eef2ff;border-bottom:1px solid var(--gray-200);border-top:2px solid var(--blue-500);
|
|
padding:9px 10px;
|
|
}
|
|
.section-row .section-bar{display:flex;align-items:center;justify-content:space-between;gap:10px;}
|
|
.section-row .section-name{display:flex;align-items:center;gap:8px;font-weight:700;color:var(--blue-800);font-size:13px;}
|
|
.section-row .section-tag{font-size:10.5px;font-weight:700;background:var(--blue-600);color:#fff;padding:2px 8px;border-radius:4px;}
|
|
.section-row .section-actions{display:flex;gap:6px;}
|
|
|
|
.order-num{color:var(--gray-500);font-size:12.5px;font-family:Consolas,monospace;padding-left:6px;}
|
|
|
|
.cell-input, .cell-select{
|
|
padding:7px 9px;border:1px solid var(--gray-300);border-radius:6px;font-size:12.5px;background:#fff;width:100%;
|
|
font-family:Consolas,monospace;
|
|
}
|
|
.cell-input.wide{min-width:150px;}
|
|
.cell-input.small{width:64px;}
|
|
.cell-select{font-family:inherit;min-width:150px;}
|
|
.cell-input:disabled, .cell-select:disabled{background:var(--gray-100);color:var(--gray-400,#94a3b8);cursor:not-allowed;}
|
|
.cell-input:focus, .cell-select:focus{outline:none;border-color:var(--blue-500);box-shadow:0 0 0 3px rgba(59,130,246,.15);}
|
|
.cell-input.error{border-color:var(--red-600);background:var(--red-50);}
|
|
|
|
.type-badge{display:inline-block;font-size:10.5px;font-weight:700;padding:2px 6px;border-radius:4px;margin-top:4px;}
|
|
.type-1{background:var(--green-50);color:var(--green-600);}
|
|
.type-3{background:var(--amber-50);color:var(--amber-600);}
|
|
.type-4{background:var(--purple-50);color:var(--purple-600);}
|
|
|
|
.icon-btn{border:1px solid var(--gray-300);background:#fff;color:var(--red-600);border-radius:6px;padding:6px 9px;font-size:12px;cursor:pointer;}
|
|
.icon-btn:hover{background:var(--red-50);border-color:#fecaca;}
|
|
.icon-btn:disabled{opacity:.35;cursor:not-allowed;}
|
|
.icon-btn.plain{color:var(--blue-600);}
|
|
.icon-btn.plain:hover{background:#eff6ff;border-color:#bfdbfe;}
|
|
|
|
.match-rule-row{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:12px 20px;margin:0 20px 0;background:#f5f3ff;border:1px solid #ddd6fe;border-radius:8px;color:#5b21b6;font-size:12.5px;}
|
|
.match-rule-row b{color:#5b21b6;}
|
|
|
|
.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:160px;}
|
|
.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;}
|
|
|
|
.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);}
|
|
|
|
.row-actions{display:flex;gap:6px;flex-wrap:wrap;}
|
|
|
|
.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-grid{display:grid;grid-template-columns:1fr 1fr 1fr auto;gap:10px;align-items:end;}
|
|
.test-grid .field label{font-size:11.5px;}
|
|
.test-grid input{padding:9px 10px;border:1px solid var(--gray-300);border-radius:6px;font-size:12.5px;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 code{background:rgba(0,0,0,.06);padding:1px 5px;border-radius:4px;}
|
|
|
|
.toolbar-row{display:flex;align-items:center;justify-content:space-between;padding:12px 20px;border-bottom:1px solid var(--gray-100);gap:12px;flex-wrap:wrap;}
|
|
.toolbar-row .left{display:flex;align-items:center;gap:10px;font-size:12.5px;color:var(--gray-500);}
|
|
|
|
.ftps-btn{
|
|
display:inline-flex;align-items:center;gap:8px;background:#fff;border:1px solid var(--gray-300);border-radius:8px;
|
|
padding:7px 12px;cursor:pointer;font-size:12.5px;color:var(--gray-600);
|
|
}
|
|
.ftps-btn:hover{border-color:var(--blue-500);background:#f8fbff;}
|
|
.ftps-btn .ftps-dest{font-family:Consolas,monospace;font-size:11.5px;color:var(--gray-500);}
|
|
.ftps-btn .ftps-dest.unset{color:var(--amber-600);}
|
|
.ftps-btn .ftps-icon{font-size:14px;}
|
|
|
|
.field{display:flex;flex-direction:column;gap:5px;}
|
|
.field.full{grid-column:1 / -1;}
|
|
.field label{font-size:12.5px;color:var(--gray-600);font-weight:600;}
|
|
.field input[type=text],.field input[type=password],.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);}
|
|
.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;}
|
|
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px 16px;}
|
|
|
|
.alert{display:flex;gap:10px;align-items:flex-start;padding:12px 16px;border-radius:8px;font-size:12.5px;line-height:1.6;margin:14px 20px 0;}
|
|
.alert-error{background:var(--red-50);border:1px solid #fecaca;color:#991b1b;}
|
|
.alert-info{background:#eff6ff;border:1px solid #bfdbfe;color:#1e40af;}
|
|
.alert .ico{font-size:15px;}
|
|
|
|
.card-foot{display:flex;justify-content:space-between;align-items:center;padding:14px 20px;border-top:1px solid var(--gray-200);flex-wrap:wrap;gap:8px;}
|
|
.card-foot .last-saved{font-size:12px;color:var(--gray-500);}
|
|
|
|
.legend{display:flex;gap:16px;flex-wrap:wrap;padding:10px 20px 0;font-size:12px;color:var(--gray-500);}
|
|
.legend span{display:inline-flex;align-items:center;gap:6px;}
|
|
.legend .dot{width:9px;height:9px;border-radius:2px;}
|
|
.dot-1{background:var(--green-600);}
|
|
.dot-3{background:var(--amber-600);}
|
|
.dot-4{background:var(--purple-600);}
|
|
|
|
.empty-state{padding:40px 20px;text-align:center;color:var(--gray-500);font-size:13px;}
|
|
|
|
.modal-overlay{position:fixed;inset:0;background:rgba(15,23,42,.5);display:none;align-items:center;justify-content:center;z-index:50;}
|
|
.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.wide{width:640px;text-align:left;}
|
|
.modal h3{margin:0 0 6px;font-size:16px;}
|
|
.modal p{margin:0 0 4px;color:var(--gray-500);font-size:13px;}
|
|
.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:220px;overflow-y:auto;}
|
|
.modal-log div{padding:2px 0;}
|
|
.modal-close{margin-top:10px;width:100%;}
|
|
.dat-pre{background:#0f172a;color:#a5f3fc;border-radius:8px;padding:16px;font-size:12px;font-family:Consolas,monospace;line-height:1.6;max-height:420px;overflow:auto;white-space:pre;}
|
|
|
|
.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">雷刻檔設定(.txt)</div>
|
|
<div class="nav-sub">加工參數檔設定(.xml)</div>
|
|
<div class="nav-sub current">加工參數檔設定(.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">
|
|
加工參數檔設定(.dat)
|
|
<div class="breadcrumb">設備加工檔 / 加工參數檔設定(.dat)</div>
|
|
</div>
|
|
|
|
<div class="content">
|
|
|
|
<div class="cat-tabs" id="catTabs"></div>
|
|
|
|
<div class="card">
|
|
<div class="card-head">
|
|
<div>
|
|
<h2 id="catTitle">—</h2>
|
|
<p>.dat 為「大標籤 [Section] > 小標籤(處方欄位)」的階層結構;小標籤(處方欄位)為主鍵,同一類別內不可重複</p>
|
|
</div>
|
|
<div class="actions">
|
|
<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-outline btn-sm" id="addSectionBtn" style="display:none;">+ 新增大標籤</button>
|
|
<button class="btn btn-danger-outline btn-sm" id="deleteCatBtn" style="display:none;">🗑 刪除此類別</button>
|
|
<button class="btn btn-purple btn-sm" id="previewDatBtn">🔍 預覽 .dat</button>
|
|
<button class="btn btn-green btn-sm" id="saveDatBtn" style="display:none;">💾 儲存</button>
|
|
</div>
|
|
</div>
|
|
<div class="alert alert-info" id="lockedHint" style="margin:0 20px 0;">
|
|
<span class="ico">🔒</span>
|
|
<div>目前為檢視模式,欄位不可編輯,「新增大標籤」與「儲存」按鈕也會暫時隱藏。請點「✎ 修改」解鎖後即可編輯。</div>
|
|
</div>
|
|
|
|
<div class="match-rule-row" id="matchRuleRow" style="display:none;">
|
|
<div style="display:flex;align-items:center;gap:8px;">
|
|
<span style="font-size:16px;">🧪</span>
|
|
<span>類別自動比對規則(Regular Expression)— 依 <b>客戶</b> / <b>產品設計碼</b> / <b>產品類別</b> 判斷是否套用此範本:目前 <b id="matchRuleCount">0</b> 條規則</span>
|
|
</div>
|
|
<button class="btn btn-outline btn-sm" id="editMatchRulesBtn">📐 設定比對規則</button>
|
|
</div>
|
|
|
|
<div class="legend">
|
|
<span><span class="dot dot-1"></span>1:處方規格內容 — 需填對應處方規格代碼</span>
|
|
<span><span class="dot dot-3"></span>3:固定值 — 需填固定值</span>
|
|
<span><span class="dot dot-4"></span>4:處方規格運算 — 需填運算代碼1 / 運算方式 / 運算代碼2</span>
|
|
</div>
|
|
|
|
<div id="dupAlert" style="display:none;"></div>
|
|
|
|
<div class="toolbar-row">
|
|
<div class="left">目前檔案:<span id="fileNameLabel" style="font-family:Consolas,monospace;"></span></div>
|
|
<div class="left">共 <b id="sectionCount">0</b> 個大標籤 / <b id="rowCount">0</b> 個小標籤</div>
|
|
<button class="ftps-btn" id="ftpsBtn">
|
|
<span class="ftps-icon">🔌</span>
|
|
<span>FTPS 設定:</span>
|
|
<span class="ftps-dest" id="ftpsDestLabel">未設定</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div style="overflow-x:auto;">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width:36px;">#</th>
|
|
<th style="min-width:180px;">小標籤(處方欄位,主鍵)</th>
|
|
<th style="min-width:170px;">資料類型</th>
|
|
<th style="min-width:160px;">對應處方規格代碼</th>
|
|
<th style="width:80px;">固定值</th>
|
|
<th style="min-width:130px;">運算代碼1</th>
|
|
<th style="min-width:110px;">運算方式</th>
|
|
<th style="min-width:130px;">運算代碼2</th>
|
|
<th style="width:44px;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="rowsBody"></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="empty-state" id="emptyState" style="display:none;">此類別尚無任何大標籤,請點「✎ 修改」後再點「+ 新增大標籤」開始建立</div>
|
|
|
|
<div class="card-foot">
|
|
<span class="last-saved" id="lastSaved">尚未儲存</span>
|
|
<span class="last-saved">上傳目的地(FTPS):<span id="serverPathLabel" style="font-family:Consolas,monospace;"></span></span>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<datalist id="specCodes"></datalist>
|
|
|
|
<!-- .dat preview modal -->
|
|
<div class="modal-overlay" id="datModal">
|
|
<div class="modal wide">
|
|
<h3 id="datModalTitle">.dat 預覽</h3>
|
|
<p id="datModalSub"></p>
|
|
<div class="dat-pre" id="datPreviewContent" style="margin-top:14px;"></div>
|
|
<button class="btn btn-gray modal-close" id="closeDatBtn">關閉</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Match rule editor modal (Regex: 客戶/產品設計碼/產品類別) -->
|
|
<div class="modal-overlay" id="matchRuleModal">
|
|
<div class="modal rule-modal">
|
|
<div class="rule-modal-head">
|
|
<div>
|
|
<h3 id="matchRuleModalTitle">類別自動比對規則</h3>
|
|
<p id="matchRuleModalSub"></p>
|
|
</div>
|
|
<button class="icon-btn plain" id="closeMatchRuleModalBtn">✕ 關閉</button>
|
|
</div>
|
|
<div class="rule-modal-body">
|
|
<div class="alert alert-info" style="margin:0 0 14px;">
|
|
<span class="ico">ℹ️</span>
|
|
<div>規則由上而下依序比對,「客戶」「產品設計碼」「產品類別」三個條件皆符合(欄位留空視為不限)的第一條規則即命中,系統將自動套用本類別作為加工參數範本。</div>
|
|
</div>
|
|
|
|
<table class="rule-table">
|
|
<thead>
|
|
<tr>
|
|
<th>順序</th>
|
|
<th style="min-width:160px;">客戶 (Regex)</th>
|
|
<th style="min-width:160px;">產品設計碼 (Regex)</th>
|
|
<th style="min-width:160px;">產品類別 (Regex)</th>
|
|
<th style="min-width:140px;">說明</th>
|
|
<th style="width:56px;">啟用</th>
|
|
<th style="width:110px;">排序</th>
|
|
<th style="width:36px;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="matchRuleTableBody"></tbody>
|
|
</table>
|
|
|
|
<button class="btn btn-outline btn-sm" id="addMatchRuleBtn" style="margin-top:12px;">+ 新增規則</button>
|
|
|
|
<div class="test-panel">
|
|
<h4>🧪 測試比對</h4>
|
|
<div class="test-grid">
|
|
<div class="field"><label>客戶</label><input type="text" id="mrTestCustomer" placeholder="例如 HOYA"></div>
|
|
<div class="field"><label>產品設計碼</label><input type="text" id="mrTestDesignCode" placeholder="例如 FK203"></div>
|
|
<div class="field"><label>產品類別</label><input type="text" id="mrTestProdCategory" placeholder="例如 一般片"></div>
|
|
<button class="btn btn-purple btn-sm" id="runMatchTestBtn">執行測試</button>
|
|
</div>
|
|
<div id="mrTestResult"></div>
|
|
</div>
|
|
</div>
|
|
<div class="rule-modal-foot">
|
|
<span style="font-size:12px;color:var(--gray-500);" id="matchRuleCountLabel"></span>
|
|
<div style="display:flex;gap:8px;">
|
|
<button class="btn btn-gray btn-sm" id="cancelMatchRuleBtn">取消</button>
|
|
<button class="btn btn-green btn-sm" id="applyMatchRuleBtn">套用規則</button>
|
|
</div>
|
|
</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>
|
|
|
|
<!-- Add section modal -->
|
|
<div class="modal-overlay" id="addSectionModal">
|
|
<div class="modal">
|
|
<h3>新增大標籤</h3>
|
|
<p>建立一個新的 [Section],之後可在其下新增小標籤(處方欄位)</p>
|
|
<div style="text-align:left;margin-top:16px;">
|
|
<div class="field">
|
|
<label>大標籤名稱</label>
|
|
<input type="text" id="newSectionName" placeholder="例如 Ortho / BACK / FRONT">
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;gap:10px;margin-top:20px;">
|
|
<button class="btn btn-gray" style="flex:1;" id="cancelAddSectionBtn">取消</button>
|
|
<button class="btn btn-green" style="flex:1;" id="confirmAddSectionBtn">建立</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add category modal -->
|
|
<div class="modal-overlay" id="addCatModal">
|
|
<div class="modal">
|
|
<h3>新增類別</h3>
|
|
<p>建立一個全新、空白的 .dat 類別,可自行新增大標籤/小標籤並儲存</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="newCatKey" placeholder="例如 RGP_Basic">
|
|
</div>
|
|
<div class="field">
|
|
<label>檔名 (.dat)</label>
|
|
<input type="text" id="newCatFile" placeholder="例如 RGP_Basic.dat">
|
|
</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>
|
|
|
|
<!-- 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="/process/OK/">
|
|
</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>
|
|
// ---- 類別(檔名)清單 ----
|
|
let categories = [
|
|
{ key:"OK", file:"OK.dat" }
|
|
];
|
|
|
|
// ---- 種子資料:依「A518-2607280001.dat」範例檔的 大標籤[Section] > 小標籤(處方欄位) 結構整理 ----
|
|
const seedData = {
|
|
"OK": [{"name": "Order", "rows": [{"tag": "Model Code", "dtype": "1:處方規格內容", "spec": "ModelCode", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 1}, {"tag": "Lot Number", "dtype": "1:處方規格內容", "spec": "LotNo", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 2}, {"tag": "SO Number", "dtype": "1:處方規格內容", "spec": "SONo", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 3}, {"tag": "Part Number", "dtype": "1:處方規格內容", "spec": "PartNo", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 4}]}, {"name": "Material", "rows": [{"tag": "Material Code", "dtype": "1:處方規格內容", "spec": "MaterialCode", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 5}]}, {"name": "Measure", "rows": [{"tag": "Measure Code", "dtype": "1:處方規格內容", "spec": "MeasureCode", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 6}]}, {"name": "Data", "rows": [{"tag": "Dia", "dtype": "1:處方規格內容", "spec": "DIA", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 7}, {"tag": "Power", "dtype": "1:處方規格內容", "spec": "Power", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 8}, {"tag": "BC", "dtype": "1:處方規格內容", "spec": "BC", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 9}]}, {"name": "Ortho", "rows": [{"tag": "Target", "dtype": "1:處方規格內容", "spec": "Target", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 10}, {"tag": "FlatK", "dtype": "1:處方規格內容", "spec": "FK", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 11}, {"tag": "Ecc", "dtype": "1:處方規格內容", "spec": "Ecc", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 12}, {"tag": "RCPlus (Flat)", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 13}, {"tag": "AC1Plus (Flat)", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 14}, {"tag": "AC2Plus (Flat)", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 15}, {"tag": "PCR (Flat)", "dtype": "1:處方規格內容", "spec": "PCR_Flat", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 16}, {"tag": "AC2AddRadius", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 17}, {"tag": "FKplusDK", "dtype": "4:處方規格運算", "spec": "", "fixed": "", "op1": "FK", "opMode": "1:加", "op2": "DK", "order": 18}, {"tag": "AC1PlusSt", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 19}, {"tag": "AC2PlusSt", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 20}, {"tag": "PeripheralDefocus", "dtype": "1:處方規格內容", "spec": "Peripheral Defocus", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 21}]}, {"name": "Thickness", "rows": [{"tag": "CenterThickness", "dtype": "1:處方規格內容", "spec": "CT", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 22}, {"tag": "EdgeThickness", "dtype": "1:處方規格內容", "spec": "Edge Th", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 23}]}, {"name": "BACK", "rows": [{"tag": "AZE", "dtype": "1:處方規格內容", "spec": "AZE", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 24}, {"tag": "SteepAZE", "dtype": "1:處方規格內容", "spec": "Steep AZE", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 25}, {"tag": "B BlendFactor", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 26}, {"tag": "EdgeR", "dtype": "3:固定值", "spec": "", "fixed": "0.07", "op1": "", "opMode": "", "op2": "", "order": 27}, {"tag": "BCR1", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 28}, {"tag": "BOZ", "dtype": "1:處方規格內容", "spec": "OZ", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 29}, {"tag": "BAsp1", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 30}, {"tag": "BJR1", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 31}, {"tag": "BCR2", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 32}, {"tag": "BCW2", "dtype": "3:固定值", "spec": "", "fixed": "0.60", "op1": "", "opMode": "", "op2": "", "order": 33}, {"tag": "BAsp2", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 34}, {"tag": "BJR2", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 35}, {"tag": "BCR3", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 36}, {"tag": "BCW3", "dtype": "3:固定值", "spec": "", "fixed": "0.70", "op1": "", "opMode": "", "op2": "", "order": 37}, {"tag": "BAsp3", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 38}, {"tag": "BJR3", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 39}, {"tag": "BCR4", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 40}, {"tag": "BCW4", "dtype": "3:固定值", "spec": "", "fixed": "0.60", "op1": "", "opMode": "", "op2": "", "order": 41}, {"tag": "BAsp4", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 42}, {"tag": "BJR4", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 43}]}, {"name": "FRONT", "rows": [{"tag": "F BlendFactor", "dtype": "3:固定值", "spec": "", "fixed": "0.60", "op1": "", "opMode": "", "op2": "", "order": 44}, {"tag": "FOZ", "dtype": "1:處方規格內容", "spec": "FOZ", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 45}, {"tag": "FAsp1", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 46}, {"tag": "FJR1", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 47}, {"tag": "FCW2", "dtype": "3:固定值", "spec": "", "fixed": "0", "op1": "", "opMode": "", "op2": "", "order": 48}, {"tag": "IZT", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 49}, {"tag": "FJR2", "dtype": "3:固定值", "spec": "", "fixed": "0", "op1": "", "opMode": "", "op2": "", "order": 50}]}]
|
|
};
|
|
|
|
// deep clone as working state
|
|
let data = JSON.parse(JSON.stringify(seedData));
|
|
let activeCat = "OK";
|
|
let savedState = {};
|
|
|
|
// ---- 編輯鎖定狀態 ----
|
|
let editMode = false;
|
|
let editSnapshot = null;
|
|
|
|
function updateEditModeUI(){
|
|
document.getElementById("editModeBtn").style.display = editMode ? "none" : "inline-flex";
|
|
document.getElementById("cancelEditBtn").style.display = editMode ? "inline-flex" : "none";
|
|
document.getElementById("addSectionBtn").style.display = editMode ? "inline-flex" : "none";
|
|
document.getElementById("deleteCatBtn").style.display = editMode ? "inline-flex" : "none";
|
|
document.getElementById("lockedHint").style.display = editMode ? "none" : "flex";
|
|
document.getElementById("matchRuleRow").style.display = editMode ? "flex" : "none";
|
|
const dupTags = findDuplicateTags(data[activeCat]||[]);
|
|
const saveBtn = document.getElementById("saveDatBtn");
|
|
saveBtn.style.display = editMode ? "inline-flex" : "none";
|
|
saveBtn.disabled = dupTags.size>0;
|
|
}
|
|
|
|
// ---- 每個類別(檔案)各自的 FTPS 上傳設定 ----
|
|
let ftpsSettings = {
|
|
OK: { host:"ftps.paramsvr.local", port:"21", user:"ftp_ok_dat", pass:"OkDatPass!234", path:"/process_dat/OK/", mode:"explicit" }
|
|
};
|
|
|
|
function ftpsIsSet(key){
|
|
const f = ftpsSettings[key];
|
|
return !!(f && f.host && f.user && f.path);
|
|
}
|
|
function ftpsDestString(key){
|
|
const f = ftpsSettings[key];
|
|
if(!ftpsIsSet(key)) return null;
|
|
return `${f.user}@${f.host}:${f.port||21}${f.path}`;
|
|
}
|
|
|
|
// ---- 類別自動比對規則(Regex):依 客戶/產品設計碼/產品類別 決定是否套用此加工參數範本 ----
|
|
let matchRules = {
|
|
OK: [
|
|
{ customer:"^(HOYA|B&L).*", designCode:"^FK.*", prodCategory:"一般片", desc:"HOYA/B&L 一般片,FK 系列設計碼", enabled:true },
|
|
{ customer:"", designCode:"", prodCategory:"^myOK.*", desc:"myOK 系列產品類別,客戶/設計碼不拘(兜底規則)", enabled:true }
|
|
]
|
|
};
|
|
|
|
function isValidRegex(pattern){
|
|
try{ new RegExp(pattern); return true; } catch(e){ return false; }
|
|
}
|
|
function fieldMatches(pattern, value){
|
|
if(!pattern || pattern.trim()==="") return true;
|
|
if(!isValidRegex(pattern)) return false;
|
|
return new RegExp(pattern).test(value||"");
|
|
}
|
|
function runMatchRuleTest(rules, customer, designCode, prodCategory){
|
|
for(const r of rules){
|
|
if(!r.enabled) continue;
|
|
if(fieldMatches(r.customer, customer) && fieldMatches(r.designCode, designCode) && fieldMatches(r.prodCategory, prodCategory)){
|
|
return r;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function updateMatchRuleSummary(){
|
|
document.getElementById("matchRuleCount").textContent = (matchRules[activeCat]||[]).length;
|
|
}
|
|
|
|
// ---- 比對規則編輯 modal ----
|
|
let editingMatchRules = [];
|
|
const matchRuleModal = document.getElementById("matchRuleModal");
|
|
|
|
function openMatchRuleModal(){
|
|
if(!editMode) return;
|
|
editingMatchRules = JSON.parse(JSON.stringify(matchRules[activeCat]||[]));
|
|
const cat = categories.find(c=>c.key===activeCat);
|
|
document.getElementById("matchRuleModalTitle").textContent = `類別自動比對規則 — ${activeCat}`;
|
|
document.getElementById("matchRuleModalSub").textContent = `符合條件時,系統將自動套用「${cat.file}」作為此工單的加工參數範本`;
|
|
document.getElementById("mrTestCustomer").value = "";
|
|
document.getElementById("mrTestDesignCode").value = "";
|
|
document.getElementById("mrTestProdCategory").value = "";
|
|
document.getElementById("mrTestResult").innerHTML = "";
|
|
renderMatchRuleTable();
|
|
matchRuleModal.classList.add("show");
|
|
}
|
|
document.getElementById("editMatchRulesBtn").addEventListener("click", openMatchRuleModal);
|
|
|
|
function renderMatchRuleTable(){
|
|
const tbody = document.getElementById("matchRuleTableBody");
|
|
tbody.innerHTML = "";
|
|
editingMatchRules.forEach((r,i)=>{
|
|
const validCustomer = isValidRegex(r.customer);
|
|
const validDesign = isValidRegex(r.designCode);
|
|
const validProd = isValidRegex(r.prodCategory);
|
|
const tr = document.createElement("tr");
|
|
tr.innerHTML = `
|
|
<td class="rule-order">${i+1}</td>
|
|
<td>
|
|
<input type="text" class="rule-input wide ${validCustomer?"":"invalid"}" data-i="${i}" data-role="customer" value="${escapeHtml(r.customer)}" placeholder="例如 ^HOYA.*(留空=不限)">
|
|
${validCustomer?"":'<div class="regex-error">⚠ 無效的正規表示式</div>'}
|
|
</td>
|
|
<td>
|
|
<input type="text" class="rule-input wide ${validDesign?"":"invalid"}" data-i="${i}" data-role="designCode" value="${escapeHtml(r.designCode)}" placeholder="例如 ^FK.*(留空=不限)">
|
|
${validDesign?"":'<div class="regex-error">⚠ 無效的正規表示式</div>'}
|
|
</td>
|
|
<td>
|
|
<input type="text" class="rule-input wide ${validProd?"":"invalid"}" data-i="${i}" data-role="prodCategory" value="${escapeHtml(r.prodCategory)}" placeholder="例如 一般片(留空=不限)">
|
|
${validProd?"":'<div class="regex-error">⚠ 無效的正規表示式</div>'}
|
|
</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 plain" data-i="${i}" data-action="move-up" ${i===0?"disabled":""} title="上移">↑</button>
|
|
<button class="icon-btn plain" data-i="${i}" data-action="move-down" ${i===editingMatchRules.length-1?"disabled":""} title="下移">↓</button>
|
|
</div>
|
|
</td>
|
|
<td><button class="icon-btn" data-i="${i}" data-action="delete-rule" title="刪除">🗑</button></td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
document.getElementById("matchRuleCountLabel").textContent = `共 ${editingMatchRules.length} 條規則`;
|
|
}
|
|
|
|
document.getElementById("matchRuleTableBody").addEventListener("input", e=>{
|
|
const role = e.target.dataset.role;
|
|
const i = e.target.dataset.i;
|
|
if(i===undefined || !role) return;
|
|
editingMatchRules[i][role] = e.target.value;
|
|
if(role==="customer" || role==="designCode" || role==="prodCategory"){
|
|
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("matchRuleTableBody").addEventListener("change", e=>{
|
|
const role = e.target.dataset.role;
|
|
const i = e.target.dataset.i;
|
|
if(role==="enabled") editingMatchRules[i].enabled = e.target.checked;
|
|
});
|
|
|
|
document.getElementById("matchRuleTableBody").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"){ editingMatchRules.splice(i,1); renderMatchRuleTable(); }
|
|
if(action==="move-up" && i>0){ [editingMatchRules[i-1],editingMatchRules[i]]=[editingMatchRules[i],editingMatchRules[i-1]]; renderMatchRuleTable(); }
|
|
if(action==="move-down" && i<editingMatchRules.length-1){ [editingMatchRules[i+1],editingMatchRules[i]]=[editingMatchRules[i],editingMatchRules[i+1]]; renderMatchRuleTable(); }
|
|
});
|
|
|
|
document.getElementById("addMatchRuleBtn").addEventListener("click", ()=>{
|
|
editingMatchRules.push({ customer:"", designCode:"", prodCategory:"", desc:"", enabled:true });
|
|
renderMatchRuleTable();
|
|
const inputs = document.querySelectorAll('#matchRuleTableBody input[data-role="customer"]');
|
|
const last = inputs[inputs.length-1];
|
|
if(last) last.focus();
|
|
});
|
|
|
|
document.getElementById("closeMatchRuleModalBtn").addEventListener("click", ()=> matchRuleModal.classList.remove("show"));
|
|
document.getElementById("cancelMatchRuleBtn").addEventListener("click", ()=> matchRuleModal.classList.remove("show"));
|
|
|
|
document.getElementById("applyMatchRuleBtn").addEventListener("click", ()=>{
|
|
const hasInvalid = editingMatchRules.some(r=>
|
|
(r.customer.trim()!=="" && !isValidRegex(r.customer)) ||
|
|
(r.designCode.trim()!=="" && !isValidRegex(r.designCode)) ||
|
|
(r.prodCategory.trim()!=="" && !isValidRegex(r.prodCategory))
|
|
);
|
|
if(hasInvalid){
|
|
showToast("有規則的正規表示式格式錯誤,請修正後再套用");
|
|
return;
|
|
}
|
|
matchRules[activeCat] = JSON.parse(JSON.stringify(editingMatchRules));
|
|
matchRuleModal.classList.remove("show");
|
|
updateMatchRuleSummary();
|
|
showToast(`已更新「${activeCat}」的比對規則,別忘了點「儲存」`);
|
|
});
|
|
|
|
document.getElementById("runMatchTestBtn").addEventListener("click", ()=>{
|
|
const customer = document.getElementById("mrTestCustomer").value;
|
|
const designCode = document.getElementById("mrTestDesignCode").value;
|
|
const prodCategory = document.getElementById("mrTestProdCategory").value;
|
|
const resultBox = document.getElementById("mrTestResult");
|
|
const result = runMatchRuleTest(editingMatchRules, customer, designCode, prodCategory);
|
|
if(result){
|
|
const idx = editingMatchRules.indexOf(result);
|
|
resultBox.innerHTML = `<div class="test-result hit">
|
|
✓ 命中規則 #${idx+1}:客戶 <code>/${escapeHtml(result.customer||".*")}/</code>、產品設計碼 <code>/${escapeHtml(result.designCode||".*")}/</code>、產品類別 <code>/${escapeHtml(result.prodCategory||".*")}/</code><br>
|
|
將自動套用本類別(${activeCat})作為加工參數範本${result.desc?`(${escapeHtml(result.desc)})`:""}
|
|
</div>`;
|
|
} else {
|
|
resultBox.innerHTML = `<div class="test-result miss">✕ 沒有規則命中,請檢查條件或新增兜底規則(三個欄位皆留空即符合所有工單)</div>`;
|
|
}
|
|
});
|
|
|
|
const dtypeOptions = ["1:處方規格內容","3:固定值","4:處方規格運算"];
|
|
const opModeOptions = ["1:加","2:減","3:乘","4:除"];
|
|
|
|
const allSpecs = [...new Set(Object.values(seedData).flat().flatMap(sec=>sec.rows).map(r=>r.spec).filter(Boolean))].sort();
|
|
document.getElementById("specCodes").innerHTML = allSpecs.map(s=>`<option value="${s}">`).join("");
|
|
|
|
function typeClass(dtype){
|
|
if(dtype.startsWith("1:")) return "type-1";
|
|
if(dtype.startsWith("3:")) return "type-3";
|
|
return "type-4";
|
|
}
|
|
|
|
function renderTabs(){
|
|
const wrap = document.getElementById("catTabs");
|
|
wrap.innerHTML = categories.map(c=>{
|
|
const secCount = data[c.key] ? data[c.key].length : 0;
|
|
return `<div class="cat-tab ${c.key===activeCat?'active':''}" data-cat="${c.key}">
|
|
${c.key} <span class="fname">${c.file}</span><span class="count">${secCount} 大標籤</span>
|
|
</div>`;
|
|
}).join("") + `<div class="cat-tab add-tab" id="addCatTabBtn">+ 新增類別</div>`;
|
|
wrap.querySelectorAll(".cat-tab[data-cat]").forEach(el=>{
|
|
el.addEventListener("click", ()=>{
|
|
if(editMode && el.dataset.cat!==activeCat){
|
|
showToast("請先儲存或取消目前的編輯,才能切換類別");
|
|
return;
|
|
}
|
|
activeCat = el.dataset.cat;
|
|
renderTabs();
|
|
renderTable();
|
|
});
|
|
});
|
|
document.getElementById("addCatTabBtn").addEventListener("click", openAddCatModal);
|
|
}
|
|
|
|
// ---- 新增空白類別 ----
|
|
const addCatModal = document.getElementById("addCatModal");
|
|
function openAddCatModal(){
|
|
if(editMode){
|
|
showToast("請先儲存或取消目前的編輯,才能新增類別");
|
|
return;
|
|
}
|
|
document.getElementById("newCatKey").value = "";
|
|
document.getElementById("newCatFile").value = "";
|
|
document.getElementById("newCatFile").dataset.touched = "";
|
|
addCatModal.classList.add("show");
|
|
setTimeout(()=> document.getElementById("newCatKey").focus(), 50);
|
|
}
|
|
document.getElementById("cancelAddCatBtn").addEventListener("click", ()=> addCatModal.classList.remove("show"));
|
|
document.getElementById("newCatKey").addEventListener("input", (e)=>{
|
|
const fileInput = document.getElementById("newCatFile");
|
|
if(!fileInput.dataset.touched){
|
|
fileInput.value = e.target.value.trim() ? `${e.target.value.trim()}.dat` : "";
|
|
}
|
|
});
|
|
document.getElementById("newCatFile").addEventListener("input", (e)=>{ e.target.dataset.touched = "1"; });
|
|
|
|
document.getElementById("confirmAddCatBtn").addEventListener("click", ()=>{
|
|
const key = document.getElementById("newCatKey").value.trim();
|
|
let file = document.getElementById("newCatFile").value.trim();
|
|
if(!key){ showToast("請輸入類別代碼"); return; }
|
|
if(categories.some(c=>c.key===key)){ showToast(`類別代碼「${key}」已存在`); return; }
|
|
if(!file){ file = `${key}.dat`; }
|
|
if(!file.toLowerCase().endsWith(".dat")){ file += ".dat"; }
|
|
|
|
categories.push({ key, file });
|
|
data[key] = [];
|
|
ftpsSettings[key] = { host:"", port:"21", user:"", pass:"", path:"", mode:"explicit" };
|
|
matchRules[key] = [];
|
|
|
|
activeCat = key;
|
|
editMode = true;
|
|
editSnapshot = { sections: [], matchRules: [] };
|
|
addCatModal.classList.remove("show");
|
|
renderTabs();
|
|
renderTable();
|
|
showToast(`已建立空白類別「${key}」,目前已進入編輯模式,可新增大標籤並儲存`);
|
|
});
|
|
|
|
// ---- 新增大標籤 ----
|
|
const addSectionModal = document.getElementById("addSectionModal");
|
|
document.getElementById("addSectionBtn").addEventListener("click", ()=>{
|
|
if(!editMode) return;
|
|
document.getElementById("newSectionName").value = "";
|
|
addSectionModal.classList.add("show");
|
|
setTimeout(()=> document.getElementById("newSectionName").focus(), 50);
|
|
});
|
|
document.getElementById("cancelAddSectionBtn").addEventListener("click", ()=> addSectionModal.classList.remove("show"));
|
|
document.getElementById("confirmAddSectionBtn").addEventListener("click", ()=>{
|
|
const name = document.getElementById("newSectionName").value.trim();
|
|
if(!name){ showToast("請輸入大標籤名稱"); return; }
|
|
const sections = data[activeCat];
|
|
if(sections.some(s=>s.name.toLowerCase()===name.toLowerCase())){ showToast(`大標籤「${name}」已存在`); return; }
|
|
sections.push({ name, rows: [] });
|
|
addSectionModal.classList.remove("show");
|
|
renderTable();
|
|
showToast(`已新增大標籤「${name}」,可在其下新增小標籤`);
|
|
});
|
|
|
|
// ---- 通用確認刪除 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;
|
|
});
|
|
|
|
// ---- 刪除整個類別(例如新增類別後發現有問題,可整份刪除)----
|
|
document.getElementById("deleteCatBtn").addEventListener("click", ()=>{
|
|
if(!editMode) return;
|
|
if(categories.length<=1){
|
|
showToast("至少需保留一個類別,無法刪除");
|
|
return;
|
|
}
|
|
const cat = categories.find(c=>c.key===activeCat);
|
|
openConfirm(
|
|
`刪除類別「${activeCat}」?`,
|
|
`即將刪除 ${cat.file},包含其所有大標籤/小標籤資料與 FTPS 設定,此動作無法復原。`,
|
|
()=>{
|
|
const deletedKey = activeCat;
|
|
categories = categories.filter(c=>c.key!==deletedKey);
|
|
delete data[deletedKey];
|
|
delete ftpsSettings[deletedKey];
|
|
delete savedState[deletedKey];
|
|
delete matchRules[deletedKey];
|
|
editMode = false;
|
|
editSnapshot = null;
|
|
activeCat = categories[0].key;
|
|
renderTabs();
|
|
renderTable();
|
|
showToast(`已刪除類別「${deletedKey}」`);
|
|
}
|
|
);
|
|
});
|
|
|
|
function findDuplicateTags(sections){
|
|
const seen = {};
|
|
const dupTags = new Set();
|
|
sections.forEach(sec=>{
|
|
sec.rows.forEach(r=>{
|
|
if(r.tag.trim()!==""){
|
|
const key = r.tag.trim().toLowerCase();
|
|
seen[key] = (seen[key]||0)+1;
|
|
if(seen[key]>1) dupTags.add(r.tag.trim());
|
|
}
|
|
});
|
|
});
|
|
return dupTags;
|
|
}
|
|
|
|
function renderTable(){
|
|
const cat = categories.find(c=>c.key===activeCat);
|
|
document.getElementById("catTitle").textContent = `${activeCat} 加工參數列表 (.dat)`;
|
|
document.getElementById("fileNameLabel").textContent = cat.file;
|
|
const dest = ftpsDestString(activeCat);
|
|
document.getElementById("serverPathLabel").textContent = dest ? `${dest}${cat.file}` : "尚未設定 FTPS";
|
|
const destLabel = document.getElementById("ftpsDestLabel");
|
|
destLabel.textContent = dest || "未設定";
|
|
destLabel.classList.toggle("unset", !dest);
|
|
const saved = savedState[activeCat];
|
|
document.getElementById("lastSaved").textContent = saved ? `上次儲存:${saved.time}` : "尚未儲存";
|
|
|
|
const sections = data[activeCat];
|
|
const dupTags = findDuplicateTags(sections);
|
|
const lockAttr = editMode ? "" : "disabled";
|
|
|
|
const body = document.getElementById("rowsBody");
|
|
body.innerHTML = "";
|
|
|
|
if(sections.length===0){
|
|
document.getElementById("emptyState").style.display = "block";
|
|
} else {
|
|
document.getElementById("emptyState").style.display = "none";
|
|
sections.forEach((sec, secIdx)=>{
|
|
const secTr = document.createElement("tr");
|
|
secTr.className = "section-row";
|
|
secTr.innerHTML = `
|
|
<td colspan="9">
|
|
<div class="section-bar">
|
|
<div class="section-name"><span class="section-tag">大標籤</span> [${escapeHtml(sec.name)}] <span style="font-weight:400;color:var(--gray-500);font-size:11.5px;">(${sec.rows.length} 個小標籤)</span></div>
|
|
<div class="section-actions">
|
|
<button class="icon-btn plain" data-sec="${secIdx}" data-action="add-field" ${lockAttr}>+ 新增小標籤</button>
|
|
<button class="icon-btn" data-sec="${secIdx}" data-action="delete-section" ${lockAttr}>🗑 刪除大標籤</button>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
`;
|
|
body.appendChild(secTr);
|
|
|
|
sec.rows.forEach((r, rowIdx)=>{
|
|
const isDup = r.tag.trim()!=="" && dupTags.has(r.tag.trim());
|
|
const isType1 = r.dtype.startsWith("1:");
|
|
const isType3 = r.dtype.startsWith("3:");
|
|
const isType4 = r.dtype.startsWith("4:");
|
|
const tr = document.createElement("tr");
|
|
tr.dataset.sec = secIdx;
|
|
tr.dataset.row = rowIdx;
|
|
if(isDup) tr.classList.add("dup");
|
|
tr.innerHTML = `
|
|
<td class="order-num">${r.order}</td>
|
|
<td><input type="text" class="cell-input wide ${isDup?'error':''}" data-role="tag" value="${escapeHtml(r.tag)}" placeholder="例如 Dia / Power / BC" ${lockAttr}></td>
|
|
<td>
|
|
<select class="cell-select" data-role="dtype" ${lockAttr}>
|
|
${dtypeOptions.map(o=>`<option value="${o}" ${o===r.dtype?"selected":""}>${o}</option>`).join("")}
|
|
</select>
|
|
<span class="type-badge ${typeClass(r.dtype)}">${r.dtype.split(":")[1]}</span>
|
|
</td>
|
|
<td><input type="text" list="specCodes" class="cell-input wide" data-role="spec" value="${escapeHtml(r.spec)}" ${(editMode && isType1)?"":"disabled"} placeholder="${isType1?'例如 DIA / BC / FK':''}"></td>
|
|
<td><input type="text" class="cell-input small" data-role="fixed" value="${escapeHtml(r.fixed)}" ${(editMode && isType3)?"":"disabled"}></td>
|
|
<td><input type="text" list="specCodes" class="cell-input wide" data-role="op1" value="${escapeHtml(r.op1)}" ${(editMode && isType4)?"":"disabled"}></td>
|
|
<td>
|
|
<select class="cell-select" data-role="opMode" ${(editMode && isType4)?"":"disabled"}>
|
|
<option value="">-</option>
|
|
${opModeOptions.map(o=>`<option value="${o}" ${o===r.opMode?"selected":""}>${o}</option>`).join("")}
|
|
</select>
|
|
</td>
|
|
<td><input type="text" list="specCodes" class="cell-input wide" data-role="op2" value="${escapeHtml(r.op2)}" ${(editMode && isType4)?"":"disabled"}></td>
|
|
<td><button class="icon-btn" data-sec="${secIdx}" data-row="${rowIdx}" data-action="delete-field" title="刪除此小標籤" ${lockAttr}>🗑</button></td>
|
|
`;
|
|
body.appendChild(tr);
|
|
});
|
|
});
|
|
}
|
|
|
|
const totalRows = sections.reduce((s,sec)=> s+sec.rows.length, 0);
|
|
document.getElementById("sectionCount").textContent = sections.length;
|
|
document.getElementById("rowCount").textContent = totalRows;
|
|
|
|
const alertBox = document.getElementById("dupAlert");
|
|
if(dupTags.size>0){
|
|
alertBox.style.display = "block";
|
|
alertBox.innerHTML = `<div class="alert alert-error"><span class="ico">⚠️</span><div>發現 ${dupTags.size} 個重複的小標籤(處方欄位):<b>${[...dupTags].join("、")}</b>。小標籤為主鍵,請修正後再儲存,目前無法轉換為 .dat。</div></div>`;
|
|
} else {
|
|
alertBox.style.display = "none";
|
|
alertBox.innerHTML = "";
|
|
}
|
|
updateMatchRuleSummary();
|
|
updateEditModeUI();
|
|
}
|
|
|
|
function escapeHtml(s){
|
|
return (s||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
|
}
|
|
|
|
// ---- 欄位編輯(文字輸入:僅更新資料 + 輕量驗證,避免整表重繪造成失焦)----
|
|
document.getElementById("rowsBody").addEventListener("input", e=>{
|
|
if(!editMode) return;
|
|
const role = e.target.dataset.role;
|
|
if(!role) return;
|
|
const tr = e.target.closest("tr");
|
|
if(tr.dataset.sec===undefined || tr.dataset.row===undefined) return;
|
|
const secIdx = Number(tr.dataset.sec);
|
|
const rowIdx = Number(tr.dataset.row);
|
|
data[activeCat][secIdx].rows[rowIdx][role] = e.target.value;
|
|
if(role==="tag") refreshValidationOnly();
|
|
});
|
|
|
|
document.getElementById("rowsBody").addEventListener("change", e=>{
|
|
if(!editMode) return;
|
|
const role = e.target.dataset.role;
|
|
if(role!=="dtype") return;
|
|
const tr = e.target.closest("tr");
|
|
const secIdx = Number(tr.dataset.sec);
|
|
const rowIdx = Number(tr.dataset.row);
|
|
data[activeCat][secIdx].rows[rowIdx].dtype = e.target.value;
|
|
renderTable();
|
|
});
|
|
|
|
document.getElementById("rowsBody").addEventListener("click", e=>{
|
|
if(!editMode) return;
|
|
const btn = e.target.closest("button[data-action]");
|
|
if(!btn) return;
|
|
const action = btn.dataset.action;
|
|
const secIdx = Number(btn.dataset.sec);
|
|
|
|
if(action==="add-field"){
|
|
const sec = data[activeCat][secIdx];
|
|
const totalRows = data[activeCat].reduce((s,x)=> s+x.rows.length, 0);
|
|
sec.rows.push({ order: totalRows+1, tag:"", dtype:"1:處方規格內容", spec:"", fixed:"", op1:"", opMode:"", op2:"" });
|
|
renderTable();
|
|
return;
|
|
}
|
|
if(action==="delete-section"){
|
|
const secName = data[activeCat][secIdx].name;
|
|
data[activeCat].splice(secIdx,1);
|
|
renumber(data[activeCat]);
|
|
renderTable();
|
|
showToast(`已刪除大標籤「${secName}」`);
|
|
return;
|
|
}
|
|
if(action==="delete-field"){
|
|
const rowIdx = Number(btn.dataset.row);
|
|
data[activeCat][secIdx].rows.splice(rowIdx,1);
|
|
renumber(data[activeCat]);
|
|
renderTable();
|
|
showToast("已刪除該小標籤");
|
|
return;
|
|
}
|
|
});
|
|
|
|
function renumber(sections){
|
|
let n = 1;
|
|
sections.forEach(sec=> sec.rows.forEach(r=> r.order = n++));
|
|
}
|
|
|
|
function refreshValidationOnly(){
|
|
const sections = data[activeCat];
|
|
const dupTags = findDuplicateTags(sections);
|
|
const body = document.getElementById("rowsBody");
|
|
[...body.children].forEach(tr=>{
|
|
if(tr.dataset.sec===undefined || tr.dataset.row===undefined) return;
|
|
const secIdx = Number(tr.dataset.sec);
|
|
const rowIdx = Number(tr.dataset.row);
|
|
const r = sections[secIdx].rows[rowIdx];
|
|
const isDup = r.tag.trim()!=="" && dupTags.has(r.tag.trim());
|
|
tr.classList.toggle("dup", isDup);
|
|
const tagInput = tr.querySelector('input[data-role="tag"]');
|
|
if(tagInput) tagInput.classList.toggle("error", isDup);
|
|
});
|
|
const alertBox = document.getElementById("dupAlert");
|
|
if(dupTags.size>0){
|
|
alertBox.style.display = "block";
|
|
alertBox.innerHTML = `<div class="alert alert-error"><span class="ico">⚠️</span><div>發現 ${dupTags.size} 個重複的小標籤(處方欄位):<b>${[...dupTags].join("、")}</b>。小標籤為主鍵,請修正後再儲存,目前無法轉換為 .dat。</div></div>`;
|
|
} else {
|
|
alertBox.style.display = "none";
|
|
alertBox.innerHTML = "";
|
|
}
|
|
document.getElementById("saveDatBtn").disabled = dupTags.size>0;
|
|
}
|
|
|
|
document.getElementById("editModeBtn").addEventListener("click", ()=>{
|
|
editMode = true;
|
|
editSnapshot = {
|
|
sections: JSON.parse(JSON.stringify(data[activeCat])),
|
|
matchRules: JSON.parse(JSON.stringify(matchRules[activeCat]||[]))
|
|
};
|
|
renderTable();
|
|
});
|
|
|
|
document.getElementById("cancelEditBtn").addEventListener("click", ()=>{
|
|
if(editSnapshot){
|
|
data[activeCat] = editSnapshot.sections;
|
|
matchRules[activeCat] = editSnapshot.matchRules;
|
|
}
|
|
editMode = false;
|
|
editSnapshot = null;
|
|
renderTable();
|
|
showToast("已取消編輯,變更未儲存");
|
|
});
|
|
|
|
// ---- .dat 預覽 ----
|
|
function buildDat(catKey){
|
|
const sections = data[catKey];
|
|
let out = "\n";
|
|
sections.forEach(sec=>{
|
|
out += `[${sec.name}]\n`;
|
|
sec.rows.forEach(r=>{
|
|
let valuePreview = "";
|
|
if(r.dtype.startsWith("1:")){
|
|
valuePreview = r.spec ? `<${escapeDat(r.spec)}>` : "<未設定處方規格代碼>";
|
|
} else if(r.dtype.startsWith("3:")){
|
|
valuePreview = r.fixed;
|
|
} else if(r.dtype.startsWith("4:")){
|
|
const modeSym = {"1:加":"+","2:減":"-","3:乘":"×","4:除":"÷"}[r.opMode] || r.opMode || "?";
|
|
valuePreview = `<${escapeDat(r.op1)||"?"} ${modeSym} ${escapeDat(r.op2)||"?"}>`;
|
|
}
|
|
out += ` ${r.tag}: ${valuePreview}\n`;
|
|
});
|
|
out += "\n";
|
|
});
|
|
return out;
|
|
}
|
|
function escapeDat(s){ return s||""; }
|
|
|
|
const datModal = document.getElementById("datModal");
|
|
document.getElementById("previewDatBtn").addEventListener("click", ()=>{
|
|
const cat = categories.find(c=>c.key===activeCat);
|
|
const totalRows = data[activeCat].reduce((s,sec)=> s+sec.rows.length, 0);
|
|
document.getElementById("datModalTitle").textContent = `.dat 預覽 — ${cat.file}`;
|
|
document.getElementById("datModalSub").textContent = `共 ${data[activeCat].length} 個大標籤、${totalRows} 個小標籤(僅預覽,尖括號 <> 內為對應規則,尚未寫入 Server)`;
|
|
document.getElementById("datPreviewContent").textContent = buildDat(activeCat);
|
|
datModal.classList.add("show");
|
|
});
|
|
document.getElementById("closeDatBtn").addEventListener("click", ()=> datModal.classList.remove("show"));
|
|
|
|
// ---- FTPS settings modal ----
|
|
const ftpsModal = document.getElementById("ftpsModal");
|
|
|
|
function openFtpsModal(){
|
|
const cat = categories.find(c=>c.key===activeCat);
|
|
const f = ftpsSettings[activeCat];
|
|
document.getElementById("ftpsModalTitle").textContent = `FTPS 連線設定 — ${activeCat}`;
|
|
document.getElementById("ftpsModalSub").textContent = `此設定僅套用於 ${cat.file},每個類別各自獨立`;
|
|
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("ftpsBtn").addEventListener("click", openFtpsModal);
|
|
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", ()=>{
|
|
ftpsSettings[activeCat] = {
|
|
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");
|
|
renderTable();
|
|
showToast(`已更新「${activeCat}」的 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 || "/"} 可讀寫`]
|
|
})
|
|
});
|
|
});
|
|
|
|
// ---- Save (convert + upload) action ----
|
|
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("saveDatBtn").addEventListener("click", ()=>{
|
|
if(!editMode) return;
|
|
const dupTags = findDuplicateTags(data[activeCat]);
|
|
if(dupTags.size>0){
|
|
showToast("小標籤(處方欄位)有重複,請先修正");
|
|
return;
|
|
}
|
|
if(!ftpsIsSet(activeCat)){
|
|
showToast("尚未設定此類別的 FTPS 連線,請先點「FTPS 設定」填寫");
|
|
openFtpsModal();
|
|
return;
|
|
}
|
|
const cat = categories.find(c=>c.key===activeCat);
|
|
const f = ftpsSettings[activeCat];
|
|
const totalRows = data[activeCat].reduce((s,sec)=> s+sec.rows.length, 0);
|
|
const path = `${ftpsDestString(activeCat)}${cat.file}`;
|
|
runModalSequence({
|
|
title:`轉換為 .dat — ${activeCat}`,
|
|
desc:`正在將 ${data[activeCat].length} 個大標籤、${totalRows} 個小標籤轉換並透過 FTPS 上傳`,
|
|
steps:[
|
|
"驗證小標籤(處方欄位)唯一性...",
|
|
"驗證各列必填欄位...",
|
|
"驗證類別自動比對規則(客戶/產品設計碼/產品類別)...",
|
|
`轉換為 .dat 結構(${cat.file})...`,
|
|
`建立 FTPS 連線至 ${f.host}:${f.port}...`,
|
|
`以帳號「${f.user}」登入並上傳至 ${f.path}${cat.file}...`
|
|
],
|
|
onDone: ()=>{
|
|
const time = new Date().toLocaleString('zh-TW',{hour12:true});
|
|
savedState[activeCat] = { time, path };
|
|
showModalResult({
|
|
title:"已透過 FTPS 上傳",
|
|
desc:`${cat.file} 已更新,共 ${data[activeCat].length} 個大標籤、${totalRows} 個小標籤`,
|
|
log:[`✓ 主鍵檢查通過`, `✓ .dat 轉換完成`, `✓ 自動比對規則 ${(matchRules[activeCat]||[]).length} 條已生效`, `✓ FTPS 已寫入 ${path}`]
|
|
});
|
|
editMode = false;
|
|
editSnapshot = null;
|
|
renderTable();
|
|
}
|
|
});
|
|
});
|
|
|
|
function showToast(msg){
|
|
const t = document.getElementById("toast");
|
|
t.textContent = "✓ " + msg;
|
|
t.classList.add("show");
|
|
setTimeout(()=>t.classList.remove("show"), 3000);
|
|
}
|
|
|
|
renderTabs();
|
|
renderTable();
|
|
</script>
|
|
</body>
|
|
</html>
|