測試輔助拍照助手
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>消防照片助手</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<style>
:root{--red:#c0392b;--green:#1a472a;--nb:#1a4a7c;--dark:#2c3e50;--bg:#f8f7f4;--card:#fff;--border:#e8e4dc;--gold:#d4a017;}
*{box-sizing:border-box;margin:0;padding:0;}
body{font-family:'Noto Sans TC','Microsoft JhengHei',-apple-system,sans-serif;background:var(--bg);color:#333;-webkit-font-smoothing:antialiased;}
.wrap{max-width:500px;margin:0 auto;padding:14px 12px 90px;min-height:100vh;}
.header{text-align:center;margin-bottom:14px;}
.badge{display:inline-block;font-size:11px;font-weight:700;letter-spacing:1px;padding:3px 10px;border-radius:16px;margin-bottom:5px;}
.badge-tp{color:var(--red);background:#fdf2f2;}
.badge-nb{color:var(--nb);background:#f0f4fa;}
h1{font-size:19px;font-weight:800;color:#1a1a1a;margin:2px 0;}
.sub{font-size:12px;color:#888;margin:2px 0;}
.card{background:var(--card);border-radius:10px;padding:13px;margin-bottom:8px;border:1px solid var(--border);}
.fl{font-size:11px;color:#888;margin-bottom:3px;font-weight:600;}
input[type=text],input[type=password],select{width:100%;padding:8px 10px;border:1px solid #ddd;border-radius:6px;font-size:13px;margin-bottom:8px;background:#fff;color:#333;}
input.dw{border-color:var(--gold);background:#fffef5;font-weight:700;color:#7d5a00;font-family:'Courier New',monospace;font-size:15px;letter-spacing:1px;}
.mbtn{width:100%;padding:13px;color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:700;cursor:pointer;margin-top:4px;}
.btn-tp{background:var(--red);}
.btn-nb{background:var(--nb);}
.btn-g{background:var(--green);}
.btn-d{background:var(--dark);}
.sb{padding:5px 11px;border-radius:6px;font-size:11px;font-weight:700;border:none;cursor:pointer;color:#fff;white-space:nowrap;}
.nb-btn{flex:1;padding:9px 6px;background:#f5f5f0;color:#333;border:1px solid #ddd;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;text-align:center;}
.nav-row{display:flex;gap:7px;margin-top:8px;}
.prog-bar{background:#e0e0d8;border-radius:8px;height:8px;margin-bottom:10px;overflow:hidden;}
.prog-fill{height:100%;border-radius:8px;transition:width .3s;}
.fill-tp{background:var(--red);}
.fill-nb{background:var(--nb);}
/* Select */
.sec-cat-label{font-size:12px;font-weight:800;color:var(--dark);margin:14px 0 6px 2px;display:flex;align-items:center;gap:6px;}
.sec-cat-label::after{content:"";flex:1;height:1px;background:#ddd;}
.sec-hd{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}
.sec-title{font-size:13px;font-weight:800;}
.sec-btns{display:flex;gap:4px;}
.item-grid{display:flex;flex-wrap:wrap;gap:5px;}
.ibtn{padding:5px 8px;border-radius:6px;font-size:11px;font-weight:500;cursor:pointer;border:1px solid #ddd;background:#f5f5f0;color:#333;text-align:left;line-height:1.4;}
.ibtn.s-tp{background:var(--green);color:#fff;border:2px solid var(--green);font-weight:700;}
.ibtn.s-nb{background:var(--nb);color:#fff;border:2px solid var(--nb);font-weight:700;}
.sbadge{font-size:9px;font-weight:700;padding:1px 3px;border-radius:3px;margin-left:3px;}
.s★{color:var(--red);background:#fdf2f2;}
.s◎{color:#888;background:#f0f0ec;}
/* Whiteboard */
.wb{background:#fffef5;border:2px solid var(--gold);position:relative;}
.wb-tag{position:absolute;top:-10px;left:14px;background:var(--gold);color:#fff;font-size:11px;font-weight:700;padding:2px 10px;border-radius:4px;}
.wb-t{width:100%;border-collapse:collapse;font-size:12px;margin-top:8px;}
.wb-t td{padding:4px 6px;border-bottom:1px solid #eee;}
.wbl{font-size:11px;color:#666;white-space:nowrap;}
.wbv{font-weight:600;}
.wbr{font-weight:800!important;color:var(--red)!important;}
.wbd{font-weight:800!important;color:#7d5a00!important;font-family:'Courier New',monospace;font-size:14px;background:#fffbf0;padding:2px 6px;border-radius:4px;border:1px solid var(--gold);}
/* Current item */
.ci{text-align:center;border-radius:10px;padding:13px;margin-bottom:8px;}
.ci.tp{background:var(--green);color:#fff;}
.ci.nb{background:var(--nb);color:#fff;}
.ci .cc{font-size:10px;opacity:.7;}
.ci .cn{font-size:15px;font-weight:800;line-height:1.4;margin:4px 0;}
.ci .cf{font-size:10px;opacity:.5;word-break:break-all;}
/* Reminder */
.rem{background:#fffbf0;border:2px solid #f39c12;border-radius:10px;padding:11px 14px;margin-bottom:10px;}
.rem-t{font-size:12px;font-weight:800;color:#e67e22;margin-bottom:8px;}
.rem-r{display:flex;align-items:center;gap:8px;margin-bottom:5px;}
.rem-c{width:20px;height:20px;border-radius:10px;color:#fff;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;flex-shrink:0;}
.rem-tx{font-size:12px;font-weight:600;color:#7d4800;}
.wm-i{background:#f0f4fa;border:1px solid #b0c8e8;border-radius:8px;padding:8px 12px;font-size:11px;color:#1a4a7c;display:flex;align-items:center;gap:8px;margin-bottom:10px;}
/* Camera */
.cam-pair{display:flex;gap:10px;margin-bottom:0;}
.cam-box{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:110px;border-radius:8px;cursor:pointer;}
.cam-box.red{border:2.5px dashed var(--red);background:#fdf8f8;}
.cam-box.blue{border:2.5px dashed var(--nb);background:#f0f4fa;}
.cam-icon{font-size:30px;margin-bottom:3px;}
.cam-lbl{font-size:12px;font-weight:700;}
.cam-sub{font-size:10px;margin-top:2px;}
.cam-box.red .cam-lbl{color:var(--red);}
.cam-box.blue .cam-lbl{color:var(--nb);}
.cam-box.red .cam-sub{color:#ccc;}
.cam-box.blue .cam-sub{color:#ccc;}
.prev img{width:100%;border-radius:8px;max-height:240px;object-fit:contain;background:#f0f0ec;}
.proc{display:flex;align-items:center;justify-content:center;min-height:100px;background:#f0f4fa;border-radius:10px;border:1px solid #b0c8e8;font-size:13px;font-weight:600;color:var(--nb);}
/* Review */
.rv-item{display:flex;gap:8px;align-items:center;padding:8px;}
.dot{width:22px;height:22px;border-radius:11px;color:#fff;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;flex-shrink:0;}
.dot-ok{background:#27ae60;}.dot-miss{background:#e74c3c;}
.rv-th{width:40px;height:40px;object-fit:cover;border-radius:6px;flex-shrink:0;}
.warn{background:#fdf2f2;border:1px solid #e74c3c;}
.warn-t{font-size:13px;font-weight:700;color:var(--red);margin-bottom:6px;}
.warn-i{font-size:11px;color:#922;margin-left:8px;margin-bottom:2px;}
/* Lock / City */
.lock-sc{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;padding:40px 20px;text-align:center;}
.lock-i{font-size:48px;margin-bottom:16px;}
.lock-title{font-size:22px;font-weight:800;margin-bottom:4px;}
.lock-sub{font-size:13px;color:#888;margin-bottom:24px;}
.lock-in{width:200px;text-align:center;font-size:18px;padding:10px;letter-spacing:4px;margin-bottom:12px;}
.lock-err{color:var(--red);font-size:12px;margin-top:4px;}
.city-sc{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:80vh;gap:16px;padding:20px;}
.city-btn{width:270px;padding:22px;border:none;border-radius:14px;font-size:17px;font-weight:800;color:#fff;cursor:pointer;letter-spacing:2px;}
.city-btn.tp{background:var(--red);}
.city-btn.nb{background:var(--nb);}
.city-lbl{font-size:11px;opacity:.7;font-weight:400;letter-spacing:0;display:block;margin-top:4px;}
/* History */
.hist-wrap{background:#f5fbf5;border:1.5px solid #a8d5a8;border-radius:9px;padding:10px 12px;margin-bottom:10px;}
.hist-lbl{font-size:11px;font-weight:800;color:#1a472a;margin-bottom:6px;}
.hist-row{display:flex;gap:6px;align-items:center;}
.hist-row select{margin-bottom:0;border-color:#a8d5a8;background:#fff;font-size:12px;color:#1a472a;font-weight:600;}
/* Filter */
.legend{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:8px;}
.li{font-size:10px;display:flex;align-items:center;gap:4px;color:#666;}
.frow{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap;align-items:center;}
.fbtn{padding:4px 10px;border-radius:12px;font-size:11px;font-weight:600;border:1px solid #ddd;background:#f5f5f0;color:#555;cursor:pointer;}
.fbtn.on{background:var(--nb);color:#fff;border-color:var(--nb);}
/* Download info */
.dl-info{background:#f0f8f0;border:1.5px solid #a8d5a8;border-radius:8px;padding:10px 14px;margin-bottom:8px;font-size:11px;color:#1a472a;}
.dl-info strong{font-family:'Courier New',monospace;display:block;margin-top:4px;font-size:12px;}
.bstack{display:flex;flex-direction:column;gap:8px;margin-top:8px;}
</style>
</head>
<body>
<div id="app" class="wrap"></div>
<script>
const PW="fire2025";

// ══════════════════════════════════════════
// 台北市清單
// ══════════════════════════════════════════
const TP_CHECKLIST=[
  {category:"共有設備",items:[
    {id:"TP-GEN-01",name:"發電機組全景(含固定基座、控制盤、充電器、蓄電池)"},
    {id:"TP-GEN-02a",name:"ATS關(含標示)"},
    {id:"TP-GEN-02b",name:"ATS開"},
    {id:"TP-GEN-03",name:"補充油箱(含油品種類)"},
  ]},
  {category:"水系統滅火設備",items:[
    {id:"TP-HYD-01",name:"室內消防栓加壓送水裝置"},
    {id:"TP-HYD-02a",name:"室內消防栓箱關"},
    {id:"TP-HYD-02b",name:"室內消防栓箱開"},
    {id:"TP-HYD-03",name:"室內消防栓測試用出水口(含標示)"},
    {id:"TP-HYD-04",name:"屋頂層消防實際射水"},
    {id:"TP-HYD-05",name:"屋頂層消防實際射水壓力"},
    {id:"TP-SPR-01",name:"自動撒水加壓送水裝置"},
    {id:"TP-SPR-02",name:"自動警報逆止閥(含制水閥標示)"},
    {id:"TP-SPR-03",name:"末端查驗管"},
    {id:"TP-SPR-04",name:"自動撒水送水口(含送水壓力)"},
    {id:"TP-SPR-05",name:"撒水頭及配管裝置實景"},
    {id:"TP-SPR-06",name:"蜂鳴器"},
  ]},
  {category:"警報設備",items:[
    {id:"TP-FA-01a",name:"火警受信總機整組關"},
    {id:"TP-FA-01b",name:"火警受信總機整組開(含蓄電池、話筒)"},
    {id:"TP-FA-02",name:"各型式探測器及配線"},
    {id:"TP-MA-01a",name:"火警綜合盤關(含標示燈、警鈴、話筒)"},
    {id:"TP-MA-01b",name:"火警綜合盤開(含配線)"},
    {id:"TP-PA-01",name:"廣播主機整組(含送話器及蓄電池)"},
    {id:"TP-PA-02",name:"各型式揚聲器"},
  ]},
  {category:"避難逃生設備",items:[
    {id:"TP-SIGN-01",name:"避難方向指示燈"},
    {id:"TP-SIGN-02",name:"出口標示燈"},
    {id:"TP-EM-01",name:"緊急照明燈"},
  ]},
  {category:"消防搶救上之必要設備",items:[
    {id:"TP-SS-01",name:"連結送水管送水口(含標示)"},
    {id:"TP-RES-01",name:"消防專用蓄水池加壓送水裝置"},
    {id:"TP-RES-02a",name:"採水口開(含標示)"},
    {id:"TP-RES-02b",name:"採水口關(含標示)"},
    {id:"TP-SMK-01",name:"排煙機組"},
    {id:"TP-SMK-02a",name:"排煙機組控制盤關"},
    {id:"TP-SMK-02b",name:"排煙機組控制盤開"},
    {id:"TP-SMK-03",name:"排煙風管配置情形"},
    {id:"TP-SMK-04",name:"偵煙式探測器及配線"},
    {id:"TP-SMK-05",name:"手動啟動裝置(含標示)"},
    {id:"TP-SMK-06",name:"防煙垂壁"},
    {id:"TP-EP-01a",name:"緊急電源插座保護箱關"},
    {id:"TP-EP-01b",name:"緊急電源插座保護箱開"},
    {id:"TP-EXT-01",name:"各型式滅火器(含標示)"},
  ]},
];

// ══════════════════════════════════════════
// 新北市清單
// ══════════════════════════════════════════
const NB_CHECKLIST=[
  {catId:"一",category:"共有設備",subs:[
    {sub:"發電機",items:[
      {id:"NB-GEN-01",name:"發電機室全景"},
      {id:"NB-GEN-02",name:"發電機組件(含固定基座、充電器、蓄電池)"},
      {id:"NB-GEN-03",name:"補充油箱(含防液堤及殘油坑)"},
      {id:"NB-GEN-04",name:"發電機室通風換氣設施"},
      {id:"NB-GEN-05",name:"發電機廢氣排氣管"},
      {id:"NB-GEN-06",name:"發電機啟動時測試(含儀表顯示情形)"},
      {id:"NB-GEN-07",name:"ATS開關箱(開、關)"},
      {id:"NB-GEN-08",name:"發電機ATS負載檢查及專用回路標示",star:true},
      {id:"NB-GEN-09",name:"發電機至ATS配管配線及ATS至各消防設備之配管配線",star:true},
    ]},
    {sub:"耐燃耐熱保護",items:[
      {id:"NB-FRC-01",name:"各系統耐燃、耐熱配線材檢查(近照)",star:true},
    ]},
  ]},
  {catId:"二",category:"滅火器設備",subs:[
    {sub:"滅火器",items:[
      {id:"NB-EXT-01",name:"滅火器(含固定情形及標示狀況)"},
    ]},
  ]},
  {catId:"三",category:"水系統滅火設備",subs:[
    {sub:"室內(外)消防栓",items:[
      {id:"NB-HYD-01",name:"消防泵浦性能測試(含流量計近照)"},
      {id:"NB-HYD-02",name:"室內(外)消防栓箱(開、關)"},
      {id:"NB-HYD-03",name:"室外消防水帶箱(開、關)"},
      {id:"NB-HYD-04",name:"送水口逆止閥(近照)及送水立管(與連結送水管共用時)",star:true},
      {id:"NB-HYD-05",name:"立管連通管(管底或屋頂)(雙立管時)",star:true},
      {id:"NB-HYD-06",name:"連結屋頂水箱配管",star:true},
      {id:"NB-HYD-07",name:"消防幹管加壓試驗(含測試壓力表近照)",star:true},
      {id:"NB-HYD-08",name:"最末端出水口放水壓力測試"},
    ]},
    {sub:"自動撒水設備",items:[
      {id:"NB-SPR-01",name:"撒水泵浦性能測試(含流量計近照)"},
      {id:"NB-SPR-02",name:"流水檢知裝置(含標示牌)及開關閥高度檢查"},
      {id:"NB-SPR-03",name:"撒水頭及配管裝置實景",star:true},
      {id:"NB-SPR-04",name:"連結屋頂水箱立管",star:true},
      {id:"NB-SPR-05",name:"立管連通管(管底或屋頂)(雙立管時)",star:true},
      {id:"NB-SPR-06",name:"自動撒水送水口(含高度)、送水口逆止閥及送水立管"},
      {id:"NB-SPR-07",name:"頂樓實際放射測試照片",star:true},
      {id:"NB-SPR-08",name:"末端查驗管實際測試"},
      {id:"NB-SPR-09",name:"預動式空氣壓縮機裝置全景"},
      {id:"NB-SPR-10",name:"密閉乾式及預動式之配管傾斜角度及排水閥標示"},
      {id:"NB-SPR-11",name:"撒水頭、一齊開放閥、手動啟動裝置、蜂鳴器及配管實景"},
      {id:"NB-SPR-12",name:"撒水幹管加壓試驗",star:true},
    ]},
    {sub:"水道連結型自動撒水",items:[
      {id:"NB-WC-01",name:"加壓送水裝置"},
      {id:"NB-WC-02",name:"連結水箱配管(含水箱)"},
      {id:"NB-WC-03",name:"撒水頭及配管裝置實景"},
      {id:"NB-WC-04",name:"末端查驗管實際測試"},
      {id:"NB-WC-05",name:"配管管徑及管號、管材檢查"},
    ]},
    {sub:"泡沫滅火設備",items:[
      {id:"NB-FOAM-01",name:"泡沫泵浦性能測試(含流量計近照)"},
      {id:"NB-FOAM-02",name:"比例混合器、Y型過濾器等五大配管閥件組"},
      {id:"NB-FOAM-03",name:"泡沫原液填充"},
      {id:"NB-FOAM-04",name:"泡沫幹管加壓試驗",star:true},
      {id:"NB-FOAM-05",name:"流水檢知裝置(含標示牌)及開關閥高度檢查"},
      {id:"NB-FOAM-06",name:"泡沫(泡水)頭及感知元件"},
      {id:"NB-FOAM-07",name:"一齊開放閥"},
      {id:"NB-FOAM-08",name:"手動啟動裝置"},
      {id:"NB-FOAM-09",name:"泡沫頭、感知元件、一齊開放閥、手動啟動裝置及配管實景",star:true},
      {id:"NB-FOAM-10",name:"泡沫消防栓箱(開、關)"},
      {id:"NB-FOAM-11",name:"蜂鳴器"},
      {id:"NB-FOAM-12",name:"泡沫設備放射測試(含空筒重、水溶液重等數值)"},
      {id:"NB-FOAM-13",name:"泡沫放射發泡倍率及25%還原時間測試",star:true},
      {id:"NB-FOAM-14",name:"最近端及最遠端泡沫放射區放射壓力測試"},
      {id:"NB-FOAM-15",name:"泡沫放射區放射後實景照片"},
    ]},
    {sub:"自動水霧設備",items:[
      {id:"NB-WF-01",name:"水霧泵浦性能測試(含流量計近照)"},
      {id:"NB-WF-02",name:"水霧設備送水口、送水口逆止閥及送水立管"},
      {id:"NB-WF-03",name:"連接屋頂水箱立管",star:true},
      {id:"NB-WF-04",name:"流水檢知裝置"},
      {id:"NB-WF-05",name:"撒水頭、感知元、蜂鳴器及配管裝置實景",star:true},
      {id:"NB-WF-06",name:"排水設備裝置全景"},
      {id:"NB-WF-07",name:"水霧設備放射實測"},
      {id:"NB-WF-08",name:"水霧幹管加壓試驗",star:true},
      {id:"NB-WF-09",name:"水霧頭最末端放水壓力測試"},
    ]},
  ]},
  {catId:"四",category:"警報設備",subs:[
    {sub:"火警自動警報設備",items:[
      {id:"NB-FA-01",name:"火警受信總(副)機整組(開、關)(含火警分區標示)"},
      {id:"NB-FA-02",name:"火警受信總(副)機操作開關高度檢查"},
      {id:"NB-FA-03",name:"火警受信總(副)機功能測試(含蓄電池、通話、迴路動作、斷線)"},
      {id:"NB-FA-04",name:"火警迴路阻抗測量(串接試驗)"},
      {id:"NB-FA-05",name:"各型式探測器(配線)"},
      {id:"NB-FA-06",name:"各型式探測器(動作試驗)"},
      {id:"NB-FA-07",name:"各棟電源回路預埋管(暗管部份)施工及管材情形",star:true,circle:true},
    ]},
    {sub:"火警發信警報設備",items:[
      {id:"NB-MA-01",name:"火警綜合盤整組(開、關)"},
      {id:"NB-MA-02",name:"火警發信機操作開關高度檢查"},
      {id:"NB-MA-03",name:"火警警鈴音壓檢查(或廣播替代)"},
      {id:"NB-MA-04",name:"火警發信機通話裝置測試",star:true},
    ]},
    {sub:"緊急廣播設備",items:[
      {id:"NB-PA-01",name:"廣播主機整組功能測試(含送話器及蓄電池)"},
      {id:"NB-PA-02",name:"緊急廣播主機操作開關高度檢查"},
      {id:"NB-PA-03",name:"緊急廣播迴路短路及斷路遮斷測試"},
      {id:"NB-PA-04",name:"各型揚聲器及音壓檢查",star:true},
      {id:"NB-PA-05",name:"緊急電話主機整組(通話測試)"},
      {id:"NB-PA-06",name:"緊急電話含標示牌(通話測試)"},
      {id:"NB-PA-07",name:"電源回路預埋管(暗管部份)施工及管材情形",star:true,circle:true},
    ]},
  ]},
  {catId:"五",category:"標示避難逃生設備",subs:[
    {sub:"出口標示燈",items:[
      {id:"NB-EXIT-01",name:"出口標示燈直線距離最遠處(圖形及顏色)"},
      {id:"NB-EXIT-02",name:"出口標示燈(閃滅或音聲引導功能)連動功能測試"},
      {id:"NB-EXIT-03",name:"出口標示燈(閃滅或音聲引導功能)遮斷功能測試"},
      {id:"NB-EXIT-04",name:"出口標示燈電源回路預埋配管(接線盒)",star:true,circle:true},
    ]},
    {sub:"避難方向指示燈",items:[
      {id:"NB-DIR-01",name:"各型避難方向指示燈"},
      {id:"NB-DIR-02",name:"避難方向指示燈電源回路預埋配管(接線盒)",star:true,circle:true},
    ]},
    {sub:"緊急照明設備",items:[
      {id:"NB-EML-01",name:"各型緊急照明燈及照度測試"},
      {id:"NB-EML-02",name:"緊急照明燈電源回路預埋配管(接線盒)",star:true,circle:true},
    ]},
    {sub:"避難器具",items:[
      {id:"NB-EQ-01",name:"各避難器具(含固定架、標示、操作說明、操作空間及開口部)"},
      {id:"NB-EQ-02",name:"避難器具裝置點至地面層之下降空間、下降空地"},
      {id:"NB-EQ-03",name:"避難器具裝置點至地面層之全景"},
      {id:"NB-EQ-04",name:"避難梯全景(含開口)"},
    ]},
    {sub:"緩降機",items:[
      {id:"NB-SD-01",name:"各緩降機荷重測試",star:true},
      {id:"NB-SD-02",name:"緩降機臂桿長度檢查"},
      {id:"NB-SD-03",name:"各緩降機固定螺栓扭力測試"},
      {id:"NB-SD-04",name:"緩降機繩長檢查全景"},
    ]},
    {sub:"救助袋",items:[
      {id:"NB-RB-01",name:"救助袋荷重測試",star:true},
      {id:"NB-RB-02",name:"救助袋袋長檢查(裝置點至地面層之全景)"},
    ]},
  ]},
  {catId:"六",category:"消防搶救上必要設備",subs:[
    {sub:"連結送水管",items:[
      {id:"NB-SS-01",name:"連結送水管出水口及水帶箱(開、關)"},
      {id:"NB-SS-02",name:"連結送水管送水口(含標示牌及高度)"},
      {id:"NB-SS-03",name:"立管連通管(管底或屋頂)(雙立管時)",star:true},
      {id:"NB-SS-04",name:"消防車實地送水實況",star:true},
      {id:"NB-SS-05",name:"屋頂測試放水實況"},
    ]},
    {sub:"專用蓄水池",items:[
      {id:"NB-RES-01",name:"加壓送水裝置性能測試(含流量計近照)"},
      {id:"NB-RES-02",name:"採水口(含啟動裝置開關)"},
      {id:"NB-RES-03",name:"實際採水測試"},
      {id:"NB-RES-04",name:"投入孔尺寸檢查"},
      {id:"NB-RES-05",name:"採水口高度檢查"},
    ]},
    {sub:"室內排煙(機械排煙)",items:[
      {id:"NB-SMK-01",name:"排煙機組"},
      {id:"NB-SMK-02",name:"排煙機控制盤(含開、關)"},
      {id:"NB-SMK-03",name:"排煙機控制盤控制開關動作測試"},
      {id:"NB-SMK-04",name:"各區防煙垂壁"},
      {id:"NB-SMK-05",name:"各區排煙風管配置情形"},
      {id:"NB-SMK-06",name:"偵煙式探測器、配線及動作測試"},
      {id:"NB-SMK-07",name:"手動啟動裝置及配線"},
      {id:"NB-SMK-08",name:"排煙閘門或排煙口(開、關)"},
      {id:"NB-SMK-09",name:"排煙閘門或排煙口尺寸檢查"},
      {id:"NB-SMK-10",name:"排煙口風量測試"},
      {id:"NB-SMK-11",name:"排煙啟動開關及控制盤電源配管(暗管)管材及施工情形",star:true,circle:true},
    ]},
    {sub:"室內排煙(自然排煙)",items:[
      {id:"NB-NS-01",name:"自然排煙窗尺寸檢查"},
      {id:"NB-NS-02",name:"自然排煙窗一起啟動開關啟動測試"},
      {id:"NB-NS-03",name:"開口部通風實景(含全開及全關各一張、手動啟動開關)"},
      {id:"NB-NS-04",name:"探測器外觀、配線及動作測試"},
    ]},
    {sub:"梯間排煙(機械排煙)",items:[
      {id:"NB-STR-01",name:"排煙機組"},
      {id:"NB-STR-02",name:"排煙機控制盤(含開、關)"},
      {id:"NB-STR-03",name:"排煙機控制盤控制開關動作測試"},
      {id:"NB-STR-04",name:"偵煙式探測器、配線及動作測試"},
      {id:"NB-STR-05",name:"手動啟動裝置及配線"},
      {id:"NB-STR-06",name:"進風、排煙閘門(開、關)"},
      {id:"NB-STR-07",name:"進風、排煙閘門風量測試"},
      {id:"NB-STR-08",name:"防火排煙閘門裝置"},
      {id:"NB-STR-09",name:"排煙室全景"},
    ]},
    {sub:"梯間排煙(自然排煙)",items:[
      {id:"NB-SNS-01",name:"自然排煙窗尺寸檢查"},
      {id:"NB-SNS-02",name:"自然排煙窗一起啟動開關啟動測試"},
      {id:"NB-SNS-03",name:"開口部通風實景(含全開及全關各一張、手動啟動開關)"},
      {id:"NB-SNS-04",name:"探測器外觀、配線及動作測試"},
    ]},
    {sub:"緊急電源插座",items:[
      {id:"NB-EP-01",name:"緊急電源插座(開、關)(含紅色表示燈)"},
      {id:"NB-EP-02",name:"緊急電源插座"},
      {id:"NB-EP-03",name:"緊急電源插座標示燈及電壓檢查"},
      {id:"NB-EP-04",name:"各棟電源回路預埋配管(暗管)施工及管材情形",star:true,circle:true},
    ]},
    {sub:"無線電通信輔助設備",items:[
      {id:"NB-RF-01",name:"消防隊專用無線電接頭保護箱(開、關)"},
      {id:"NB-RF-02",name:"洩波同軸電纜配線"},
      {id:"NB-RF-03",name:"蓄電池組"},
    ]},
  ]},
  {catId:"八",category:"化學系統滅火設備",subs:[
    {sub:"乾粉、CO₂、惰性氣體、鹵化烴",items:[
      {id:"NB-CHEM-01",name:"鋼瓶數(藥劑量含標示板並標明數量)"},
      {id:"NB-CHEM-02",name:"配管實景照片"},
      {id:"NB-CHEM-03",name:"噴頭"},
      {id:"NB-CHEM-04",name:"探測器動作測試"},
      {id:"NB-CHEM-05",name:"受信總機(含動作測試)"},
      {id:"NB-CHEM-06",name:"選擇閥"},
      {id:"NB-CHEM-07",name:"放射警告裝置"},
      {id:"NB-CHEM-08",name:"通風換氣裝置停止"},
      {id:"NB-CHEM-09",name:"控制盤"},
      {id:"NB-CHEM-10",name:"實際放射及氣密測試(惰性氣體、鹵化烴需檢附)"},
      {id:"NB-CHEM-11",name:"啟動鋼瓶容量檢查(一公升以上)及啟動鋼瓶配管"},
      {id:"NB-CHEM-12",name:"選擇閥檢查(含標示牌及防護區域名稱)"},
      {id:"NB-CHEM-13",name:"自動啟動測試(雙回路探測器測試)"},
      {id:"NB-CHEM-14",name:"排放裝置開關檢查(含標示)及動作測試(含風量測試)"},
    ]},
    {sub:"簡易自動滅火設備",items:[
      {id:"NB-SE-01",name:"藥劑鋼瓶(含標示)"},
      {id:"NB-SE-02",name:"噴頭及感知元件"},
      {id:"NB-SE-03",name:"手動啟動開關"},
      {id:"NB-SE-04",name:"啟動用氣體鋼瓶(含標示)"},
    ]},
  ]},
  {catId:"補",category:"其他補充相片",subs:[
    {sub:"其他補充相片",items:[
      {id:"NB-MISC-01",name:"監造人與建築物外觀及門牌合照"},
      {id:"NB-MISC-02",name:"消防立管配管(管號),監造人入鏡顯示管號名稱",star:true},
      {id:"NB-MISC-03",name:"各類型消防安全設備個別認可及名牌",star:true},
      {id:"NB-MISC-04",name:"各系統接地阻抗、絕緣阻抗試驗、回路串接試驗",star:true},
    ]},
  ]},
];

// ══════════════════════════════════════════
// 展平索引(供按鈕傳遞 index)
// ══════════════════════════════════════════
const NB_SECS=NB_CHECKLIST.flatMap(c=>c.subs.map(s=>({catId:c.catId,category:c.category,sub:s.sub,items:s.items})));
function nbAll(){return NB_SECS.flatMap(s=>s.items.map(i=>({...i,category:s.category,catId:s.catId,sub:s.sub})));}
function tpAll(){return TP_CHECKLIST.flatMap(c=>c.items.map(i=>({...i,category:c.category})));}
function safeName(n){return n.replace(/[\/\\:*?"<>|]/g,"_").trim()||"其他";}

// ══════════════════════════════════════════
// State
// ══════════════════════════════════════════
let S={
  screen:"lock",pw:"",pwError:false,
  city:"",
  projectName:"",projectAddress:"",
  inspDate:"",
  supervisorName:"",testerName:"",builderName:"",
  caseType:"",
  selectedIds:new Set(),
  photos:{},locations:{},currentIdx:0,
  nbFilter:"all",processing:false,downloading:false,
  projectHistory:undefined,
};

function normDate(v){if(!v)return"";const m=v.match(/^(\d{4})[.\-\/](\d{1,2})[.\-\/](\d{1,2})$/);return m?`${m[1]}.${m[2].padStart(2,"0")}.${m[3].padStart(2,"0")}`:v;}

// ══════════════════════════════════════════
// 持久記憶體
// ══════════════════════════════════════════
async function tryLoadHistory(){
  try{const r=await window.storage.get("fire-projects");S.projectHistory=r?JSON.parse(r.value):null;}
  catch(e){S.projectHistory=null;}
  render();
}
function applyHistory(idx){
  if(idx===""||idx===null||idx===undefined)return;
  const h=S.projectHistory?.[Number(idx)];if(!h)return;
  Object.assign(S,h);render();
}

// ══════════════════════════════════════════
// Render Router
// ══════════════════════════════════════════
function render(){
  const el=document.getElementById("app");
  if(S.screen==="lock")    return renderLock(el);
  if(S.screen==="city")    return renderCity(el);
  if(S.screen==="project") return renderProject(el);
  if(S.screen==="select")  return renderSelect(el);
  if(S.screen==="shoot")   return renderShoot(el);
  if(S.screen==="review")  return renderReview(el);
}

// ── Lock ──────────────────────────────────
function renderLock(el){
  el.innerHTML=`<div class="lock-sc">
    <div class="lock-i">🔒</div>
    <div class="lock-title">消防照片助手</div>
    <div class="lock-sub">台北市・新北市 竣工查驗</div>
    <input type="password" class="lock-in" id="pwIn" placeholder="密碼">
    <button class="mbtn btn-tp" style="width:200px" onclick="tryLogin()">進入</button>
    ${S.pwError?'<div class="lock-err">密碼錯誤</div>':''}
  </div>`;
  const i=document.getElementById("pwIn");i.focus();i.onkeydown=e=>{if(e.key==="Enter")tryLogin();};
}
function tryLogin(){
  if(document.getElementById("pwIn").value===PW){S.screen="city";S.pwError=false;}else S.pwError=true;render();
}

// ── City ──────────────────────────────────
function renderCity(el){
  el.innerHTML=`<div class="city-sc">
    <div style="font-size:26px;margin-bottom:6px">🚒</div>
    <div style="font-size:18px;font-weight:800;margin-bottom:22px">選擇縣市</div>
    <button class="city-btn tp" onclick="setCity('TP')">台北市<span class="city-lbl">照片黏貼成冊 / 白板入鏡</span></button>
    <button class="city-btn nb" onclick="setCity('NB')">新北市<span class="city-lbl">上傳消防局平台 / 依審查順序表命名</span></button>
  </div>`;
}
function setCity(c){S.city=c;S.selectedIds=new Set();S.photos={};S.locations={};S.currentIdx=0;S.screen="project";render();}

// ── Project ───────────────────────────────
function renderProject(el){
  if(S.projectHistory===undefined)tryLoadHistory();
  const isNB=S.city==="NB",hist=S.projectHistory;
  el.innerHTML=`
  <div class="header">
    <div class="badge ${isNB?"badge-nb":"badge-tp"}">${isNB?"新北市":"台北市"} ▸ 案件資訊</div>
    <h1>填寫案件資訊</h1>
    <p class="sub">檢查日期採西元紀年,同時作為標示牌日期與照片浮水印</p>
  </div>
  ${hist&&hist.length?`<div class="hist-wrap">
    <div class="hist-lbl">📂 帶入歷史案件</div>
    <div class="hist-row">
      <select id="histSel">
        <option value="">— 選擇歷史案件 —</option>
        ${hist.map((h,i)=>`<option value="${i}">${h.projectName||"(未命名)"}・${h.city==="NB"?"新北":"台北"}・${h.inspDate||""}・監造 ${h.supervisorName||"—"}</option>`).join("")}
      </select>
      <button class="sb" style="background:#1a472a" onclick="applyHistory(document.getElementById('histSel').value)">帶入</button>
    </div>
  </div>`:""}
  <div class="card">
    <div class="fl">案場名稱 *</div>
    <input type="text" id="f_name" value="${S.projectName}" placeholder="例:國立○○大樓">
    <div class="fl">場所地址 *</div>
    <input type="text" id="f_addr" value="${S.projectAddress}" placeholder="例:新北市板橋區○○路1號B1F">
    <div class="fl" style="color:#7d5a00;font-weight:800">📅 檢查日期(西元紀年)* — 同時用於標示牌與浮水印</div>
    <input type="text" id="f_date" class="dw" value="${S.inspDate}" placeholder="例:2026.03.29" maxlength="10">
    <div style="font-size:10px;color:#888;margin-top:-6px;margin-bottom:10px">格式 YYYY.MM.DD,月日不足兩位自動補零</div>
    ${isNB?`<div class="fl">案件類別</div><input type="text" id="f_ctype" value="${S.caseType}" placeholder="□使用執照 □變更使用執照 □室內裝修 □分(併)戶">`:""}
    <div style="height:6px"></div>
    <div class="fl">監造人(消防設備師)</div>
    <input type="text" id="f_super" value="${S.supervisorName}" placeholder="例:吳○○">
    <div class="fl">測試人(消防設備士)</div>
    <input type="text" id="f_tester" value="${S.testerName}" placeholder="例:秦○○">
    <div class="fl">起造人(選填)</div>
    <input type="text" id="f_builder" value="${S.builderName}" placeholder="">
  </div>
  <button class="mbtn ${isNB?"btn-nb":"btn-tp"}" onclick="saveProject()">下一步:選擇檢附項目 →</button>
  <button class="nb-btn" style="margin-top:8px" onclick="S.screen='city';render()">← 重選縣市</button>`;
}
async function saveProject(){
  S.projectName=document.getElementById("f_name").value.trim();
  S.projectAddress=document.getElementById("f_addr").value.trim();
  S.inspDate=normDate(document.getElementById("f_date").value.trim());
  S.supervisorName=document.getElementById("f_super").value.trim();
  S.testerName=document.getElementById("f_tester").value.trim();
  S.builderName=document.getElementById("f_builder").value.trim();
  const ct=document.getElementById("f_ctype");if(ct)S.caseType=ct.value.trim();
  if(!S.projectName||!S.projectAddress){alert("請填寫案場名稱和地址");return;}
  if(!S.inspDate){alert("請填寫檢查日期,例:2026.03.29");return;}
  try{
    const d={city:S.city,projectName:S.projectName,projectAddress:S.projectAddress,
      inspDate:S.inspDate,supervisorName:S.supervisorName,testerName:S.testerName,
      builderName:S.builderName,caseType:S.caseType,
      savedAt:new Date().toLocaleDateString("zh-TW",{year:"numeric",month:"2-digit",day:"2-digit"})};
    const prev=Array.isArray(S.projectHistory)?S.projectHistory:[];
    const deduped=prev.filter(h=>!(h.projectName===d.projectName&&h.projectAddress===d.projectAddress));
    const next=[d,...deduped].slice(0,5);
    await window.storage.set("fire-projects",JSON.stringify(next));
    S.projectHistory=next;
  }catch(e){}
  S.screen="select";render();
}

// ── Select ────────────────────────────────
function renderSelect(el){return S.city==="TP"?renderSelectTP(el):renderSelectNB(el);}

function renderSelectTP(el){
  let h=`<div class="header">
    <div class="badge badge-tp">台北市 ▸ ${S.projectName}</div>
    <h1>選擇檢附項目</h1>
    <p class="sub">已選 ${S.selectedIds.size} 項</p>
  </div>`;
  TP_CHECKLIST.forEach((cat,ci)=>{
    const ids=cat.items.map(i=>i.id);
    h+=`<div class="card"><div class="sec-hd">
      <span class="sec-title" style="color:var(--green)">${cat.category}</span>
      <div class="sec-btns">
        <button class="sb" style="background:#1a472a" onclick="setSecTP(${ci},true)">全選</button>
        <button class="sb" style="background:#c0392b" onclick="setSecTP(${ci},false)">取消</button>
      </div></div>
      <div class="item-grid">`;
    cat.items.forEach(i=>{const s=S.selectedIds.has(i.id);
      h+=`<button class="ibtn ${s?"s-tp":""}" onclick="toggleItem('${i.id}')">${s?"✓ ":""}${i.name}</button>`;
    });
    h+=`</div></div>`;
  });
  h+=`<div class="nav-row">
    <button class="nb-btn" onclick="S.screen='project';render()">← 修改案件</button>
    ${S.selectedIds.size>0?`<button class="mbtn btn-tp" style="flex:2" onclick="startShoot()">開始拍照(${S.selectedIds.size}項)→</button>`:""}
  </div>`;
  el.innerHTML=h;
}

function renderSelectNB(el){
  let h=`<div class="header">
    <div class="badge badge-nb">新北市 ▸ ${S.projectName}</div>
    <h1>選擇檢附項目</h1>
    <p class="sub">已選 ${S.selectedIds.size} 項</p>
  </div>
  <div class="legend">
    <div class="li"><span class="sbadge s★">★</span>既有設備可免附</div>
    <div class="li"><span class="sbadge s◎">◎</span>耐燃耐熱鋪設可免附</div>
  </div>
  <div class="frow">
    <button class="fbtn ${S.nbFilter==="all"?"on":""}" onclick="S.nbFilter='all';render()">顯示全部</button>
    <button class="fbtn ${S.nbFilter==="nostar"?"on":""}" onclick="S.nbFilter='nostar';render()">隱藏★◎</button>
    <button class="sb" style="background:#1a4a7c" onclick="quickNB()">快選非★項目</button>
  </div>`;

  let lastCat="";
  NB_SECS.forEach((sec,si)=>{
    const vis=S.nbFilter==="nostar"?sec.items.filter(i=>!i.star&&!i.circle):sec.items;
    if(!vis.length)return;
    if(sec.category!==lastCat){
      h+=`<div class="sec-cat-label">(${sec.catId})${sec.category}</div>`;
      lastCat=sec.category;
    }
    const ids=sec.items.map(i=>i.id);
    const allSel=ids.every(id=>S.selectedIds.has(id));
    h+=`<div class="card"><div class="sec-hd">
      <span class="sec-title" style="color:var(--nb)">${sec.sub||sec.category}</span>
      <div class="sec-btns">
        <button class="sb" style="background:#1a4a7c" onclick="setSecNB(${si},true)">全選</button>
        <button class="sb" style="background:#c0392b" onclick="setSecNB(${si},false)">取消</button>
      </div></div>
      <div class="item-grid">`;
    vis.forEach(i=>{
      const sel=S.selectedIds.has(i.id);
      let bx="";if(i.star)bx+=`<span class="sbadge s★">★</span>`;if(i.circle)bx+=`<span class="sbadge s◎">◎</span>`;
      h+=`<button class="ibtn ${sel?"s-nb":""}" onclick="toggleItem('${i.id}')">${sel?"✓ ":""}${i.name}${bx}</button>`;
    });
    h+=`</div></div>`;
  });
  h+=`<div class="nav-row">
    <button class="nb-btn" onclick="S.screen='project';render()">← 修改案件</button>
    ${S.selectedIds.size>0?`<button class="mbtn btn-nb" style="flex:2" onclick="startShoot()">開始拍照(${S.selectedIds.size}項)→</button>`:""}
  </div>`;
  el.innerHTML=h;
}

function setSecTP(ci,sel){const c=TP_CHECKLIST[ci];if(!c)return;c.items.forEach(i=>sel?S.selectedIds.add(i.id):S.selectedIds.delete(i.id));render();}
function setSecNB(si,sel){const s=NB_SECS[si];if(!s)return;s.items.forEach(i=>sel?S.selectedIds.add(i.id):S.selectedIds.delete(i.id));render();}
function quickNB(){nbAll().filter(i=>!i.star&&!i.circle).forEach(i=>S.selectedIds.add(i.id));render();}
function toggleItem(id){S.selectedIds.has(id)?S.selectedIds.delete(id):S.selectedIds.add(id);render();}
function startShoot(){S.currentIdx=0;S.screen="shoot";render();}

// ── Shoot ─────────────────────────────────
function getSelected(){return S.city==="TP"?tpAll().filter(i=>S.selectedIds.has(i.id)):nbAll().filter(i=>S.selectedIds.has(i.id));}
function nbFname(item,idx){const seq=String(idx+1).padStart(2,"0"),sub=item.sub?`_${item.sub}`:"";return`${seq}_${item.catId}${item.category}${sub}_${item.name}`;}
function tpFname(item,idx){return`${String(idx+1).padStart(2,"0")}_${item.category}_${item.name}`;}
function getFolder(item){return S.city==="NB"?safeName(item.sub||item.category):safeName(item.category);}

function renderShoot(el){
  const items=getSelected(),item=items[S.currentIdx];if(!item)return;
  const photo=S.photos[item.id],progress=Object.keys(S.photos).length,total=items.length;
  const loc=S.locations[item.id]||"",isNB=S.city==="NB";
  const fname=isNB?nbFname(item,S.currentIdx):tpFname(item,S.currentIdx);

  let h=`
  <div class="prog-bar"><div class="prog-fill ${isNB?"fill-nb":"fill-tp"}" style="width:${(progress/total)*100}%"></div></div>
  <div style="text-align:center;font-size:12px;color:#888;margin-bottom:8px">${progress}/${total} 已拍攝 ・ 第 ${S.currentIdx+1}/${total} 項</div>

  <div class="card wb">
    <div class="wb-tag">📋 白板填寫提示</div>
    <table class="wb-t">
      <tr><td class="wbl">案場名稱</td><td class="wbv">${S.projectName}</td></tr>
      <tr><td class="wbl">場所地址</td><td class="wbv">${S.projectAddress}</td></tr>`;
  if(!isNB){
    h+=`<tr><td class="wbl">起 造 人</td><td class="wbv">${S.builderName||"—"}</td></tr>
      <tr><td class="wbl">監 造 人</td><td class="wbv">${S.supervisorName} 消防設備師</td></tr>
      <tr><td class="wbl">測 試 人</td><td class="wbv wbr">${S.testerName} 消防設備士</td></tr>
      <tr><td class="wbl">測試設備</td><td class="wbv wbr">${item.category}</td></tr>
      <tr><td class="wbl">測試項目</td><td class="wbv wbr">${item.name}</td></tr>
      <tr><td class="wbl">檢查位置</td><td><input type="text" id="locIn" value="${loc}" placeholder="例:B1樓" style="padding:4px;font-size:12px;font-weight:700;color:var(--red);width:100%;margin:0;border-color:#ddd"></td></tr>
      <tr><td class="wbl">檢查日期</td><td><span class="wbd">${S.inspDate}</span></td></tr>
      <tr><td class="wbl">檢查現況</td><td class="wbv">☑ 竣工測試</td></tr>`;
  }else{
    h+=`<tr><td class="wbl">監 造 人</td><td class="wbv wbr">${S.supervisorName} 消防設備師(須入鏡)</td></tr>
      <tr><td class="wbl">測 試 人</td><td class="wbv wbr">${S.testerName} 消防設備士(須入鏡)</td></tr>
      <tr><td class="wbl">拍攝項目</td><td class="wbv wbr">${item.name}</td></tr>
      ${item.sub?`<tr><td class="wbl">設備類別</td><td class="wbv">${item.sub}</td></tr>`:""}
      <tr><td class="wbl">拍攝位置</td><td><input type="text" id="locIn" value="${loc}" placeholder="例:B1F、3F走廊" style="padding:4px;font-size:12px;font-weight:700;color:var(--nb);width:100%;margin:0;border-color:#ddd"></td></tr>
      <tr><td class="wbl">檢查日期</td><td><span class="wbd">${S.inspDate}</span></td></tr>`;
  }
  h+=`</table></div>

  <div class="ci ${isNB?"nb":"tp"}">
    <div class="cc">${item.category}${item.sub?" ▸ "+item.sub:""} / 資料夾:${getFolder(item)}</div>
    <div class="cn">${item.name}</div>
    <div class="cf">${fname}.jpg</div>
  </div>

  <div class="rem">
    <div class="rem-t">📸 拍照前確認</div>
    <div class="rem-r"><div class="rem-c" style="background:#2980b9">項</div><div class="rem-tx">測試項目:<strong style="color:#1a4a7c">${item.name}</strong></div></div>
    <div class="rem-r"><div class="rem-c" style="background:#2980b9">位</div><div class="rem-tx">測試位置:<strong style="color:#1a4a7c">${loc||'⚠ 請先在白板欄填寫位置'}</strong></div></div>
    <div class="rem-r"><div class="rem-c" style="background:#f39c12">✓</div><div class="rem-tx">測試人入鏡:${S.testerName} 消防設備士</div></div>
    <div class="rem-r"><div class="rem-c" style="background:#f39c12">✓</div><div class="rem-tx">標示牌入鏡(含案場名稱)</div></div>
    <div class="rem-r"><div class="rem-c" style="background:var(--gold)">日</div><div class="rem-tx">標示牌日期:<strong style="font-family:'Courier New',monospace;color:#7d5a00">${S.inspDate}</strong></div></div>
    ${isNB?`<div class="rem-r"><div class="rem-c" style="background:#f39c12">✓</div><div class="rem-tx">監造人入鏡:${S.supervisorName} 消防設備師</div></div>`:""}
  </div>
  <div class="wm-i">🖼️ 拍照後自動在右下角加上浮水印 <strong>${S.inspDate}</strong></div>`;

  if(S.processing){
    h+=`<div class="proc">⏳ 正在加入日期浮水印…</div>`;
  }else if(photo){
    h+=`<div class="card prev">
      <img src="${photo.url}" alt="">
      <div style="text-align:center;margin-top:6px;font-size:11px;color:#27ae60;font-weight:700">✓ 已拍攝(含浮水印)· 資料夾:${getFolder(item)}</div>
      <div style="display:flex;gap:6px;margin-top:6px">
        <button class="sb" style="flex:1;background:#555;font-size:12px" onclick="document.getElementById('camC').click()">📷 重拍</button>
        <button class="sb" style="flex:1;background:#1a4a7c;font-size:12px" onclick="document.getElementById('camF').click()">📁 換圖</button>
        <button class="sb" style="flex:1;background:#1a472a;font-size:12px" onclick="downloadPhoto('${item.id}')">💾 下載</button>
      </div>
    </div>`;
  }else{
    h+=`<div class="card" style="padding:12px"><div class="cam-pair">
      <div class="cam-box red" onclick="document.getElementById('camC').click()">
        <div class="cam-icon">📷</div><div class="cam-lbl">點擊拍照</div><div class="cam-sub">開啟相機</div>
      </div>
      <div class="cam-box blue" onclick="document.getElementById('camF').click()">
        <div class="cam-icon">📁</div><div class="cam-lbl">選取檔案</div><div class="cam-sub">相簿 / 檔案</div>
      </div>
    </div></div>`;
  }
  h+=`<input type="file" id="camC" accept="image/*" capture="environment" style="display:none" onchange="handleCapture(this)">
  <input type="file" id="camF" accept="image/*" style="display:none" onchange="handleCapture(this)">
  <div class="nav-row">
    <button class="nb-btn" ${S.currentIdx===0?"disabled style='opacity:.3'":""} onclick="navShoot(-1)">← 上一項</button>
    <button class="nb-btn" onclick="S.screen='select';render()" style="background:#e8f0f8;color:#1a4a7c;border-color:#b0c8e8">📋 改選項目</button>
    <button class="nb-btn" ${S.currentIdx>=total-1?"disabled style='opacity:.3'":""} onclick="navShoot(1)">跳過 →</button>
  </div>`;
  if(progress>0)h+=`<button class="mbtn btn-d" style="margin-top:8px" onclick="S.screen='review';render()">檢視全部照片 →</button>`;
  el.innerHTML=h;
}

// roundRect polyfill
if(!CanvasRenderingContext2D.prototype.roundRect){
  CanvasRenderingContext2D.prototype.roundRect=function(x,y,w,h,r){
    r=Math.min(r,w/2,h/2);this.beginPath();this.moveTo(x+r,y);
    this.arcTo(x+w,y,x+w,y+h,r);this.arcTo(x+w,y+h,x,y+h,r);
    this.arcTo(x,y+h,x,y,r);this.arcTo(x,y,x+w,y,r);this.closePath();return this;
  };
}
async function addWM(file,ds){
  return new Promise(res=>{
    const img=new Image(),tmp=URL.createObjectURL(file);
    img.onload=()=>{
      const c=document.createElement("canvas");c.width=img.naturalWidth;c.height=img.naturalHeight;
      const ctx=c.getContext("2d");ctx.drawImage(img,0,0);URL.revokeObjectURL(tmp);
      const fs=Math.max(Math.round(img.naturalWidth*0.038),24);
      ctx.font=`bold ${fs}px 'Courier New',Courier,monospace`;
      const tw=ctx.measureText(ds).width,pad=Math.round(fs*0.5),mg=Math.round(img.naturalWidth*0.018);
      const bx=img.naturalWidth-tw-pad*2-mg,by=img.naturalHeight-fs*1.6-mg;
      ctx.fillStyle="rgba(0,0,0,0.60)";ctx.roundRect(bx-pad*0.4,by,tw+pad*2.8,fs*1.55,fs*0.2);ctx.fill();
      ctx.fillStyle="#FFD700";ctx.shadowColor="rgba(0,0,0,0.5)";ctx.shadowBlur=3;
      ctx.fillText(ds,bx+pad,by+fs*1.2);ctx.shadowBlur=0;
      c.toBlob(blob=>res({url:URL.createObjectURL(blob),blob}),"image/jpeg",0.93);
    };
    img.onerror=()=>res(null);img.src=tmp;
  });
}
async function handleCapture(input){
  const file=input.files?.[0];if(!file)return;
  const items=getSelected(),item=items[S.currentIdx];if(!item)return;
  const locEl=document.getElementById("locIn");if(locEl)S.locations[item.id]=locEl.value;
  S.processing=true;render();
  const r=await addWM(file,S.inspDate);S.processing=false;
  const idx=S.currentIdx,fname=S.city==="NB"?nbFname(item,idx):tpFname(item,idx);
  S.photos[item.id]={url:r?r.url:URL.createObjectURL(file),blob:r?.blob,name:fname,folder:getFolder(item),timestamp:new Date().toISOString()};
  if(S.currentIdx<items.length-1)S.currentIdx++;
  input.value="";render();
}
function navShoot(dir){
  const locEl=document.getElementById("locIn"),items=getSelected(),item=items[S.currentIdx];
  if(locEl&&item)S.locations[item.id]=locEl.value;
  const n=S.currentIdx+dir;if(n>=0&&n<items.length){S.currentIdx=n;render();}
}

// ── Review ────────────────────────────────
function renderReview(el){
  const items=getSelected(),progress=Object.keys(S.photos).length,total=items.length;
  const missing=items.filter(i=>!S.photos[i.id]),isNB=S.city==="NB";
  const zipName=`消防照片_${S.projectName}_${S.inspDate}.zip`;

  let h=`<div class="header">
    <div class="badge ${isNB?"badge-nb":"badge-tp"}">${isNB?"新北市":"台北市"} ▸ 檢視確認</div>
    <h1>${S.projectName}</h1>
    <p class="sub">${S.projectAddress}</p>
    <p class="sub">檢查日期:<strong style="font-family:'Courier New',monospace;color:#7d5a00">${S.inspDate}</strong></p>
    <p class="sub">${progress}/${total} 已拍攝</p>
  </div>`;

  // 下載路徑說明
  const folders=[...new Set(items.filter(i=>S.photos[i.id]).map(i=>S.photos[i.id].folder||getFolder(i)))];
  if(folders.length){
    h+=`<div class="dl-info">📦 下載後解壓縮可得各設備資料夾(共 ${folders.length} 個):
      <strong>${folders.slice(0,5).join(" / ")}${folders.length>5?" …":""}</strong>
      <span style="font-size:10px;color:#888;display:block;margin-top:4px">壓縮檔:${zipName}</span>
    </div>`;
  }

  items.forEach((item,idx)=>{
    const photo=S.photos[item.id];
    h+=`<div class="card rv-item">
      <div class="dot ${photo?"dot-ok":"dot-miss"}">${photo?"✓":"!"}</div>
      ${photo?`<img src="${photo.url}" class="rv-th" onclick="downloadPhoto('${item.id}')" style="cursor:pointer" title="點擊下載">`:""}
      <div style="flex:1;min-width:0">
        <div style="font-size:12px;font-weight:600">${String(idx+1).padStart(2,"0")}. ${item.name}</div>
        <div style="font-size:10px;color:#999">${item.category}${item.sub?" ▸ "+item.sub:""}</div>
        ${photo?`<div style="font-size:10px;color:${isNB?"#1a4a7c":"#1a472a"}">📁 ${photo.folder||getFolder(item)} / ${photo.name}.jpg</div>`:""}
      </div>
      ${photo?`<button class="sb" style="background:#1a472a;flex-shrink:0;font-size:11px;padding:5px 9px" onclick="downloadPhoto('${item.id}')">💾</button>`:""}
    </div>`;
  });

  if(missing.length){
    h+=`<div class="card warn"><div class="warn-t">⚠ 尚有 ${missing.length} 項未拍攝</div>`;
    missing.forEach(m=>{h+=`<div class="warn-i">• ${m.name}</div>`;});
    h+=`<button class="sb" style="background:#c0392b;margin-top:8px" onclick="goToMissing()">繼續拍攝未完成項目</button></div>`;
  }

  h+=`<div class="bstack">
    ${S.downloading?`<div class="proc">⏳ 正在產生 ZIP 壓縮檔,請稍候…</div>`:`
    ${isNB?`<button class="mbtn btn-nb" onclick="exportNBList()">📋 匯出新北市檔名清單(TXT)</button>`:""}
    <button class="mbtn btn-g" onclick="downloadZip()">📦 下載照片 ZIP(含各設備資料夾)</button>
    <button class="mbtn btn-d" onclick="exportJSON()">📄 匯出清單 JSON</button>`}
    <div class="nav-row">
      <button class="nb-btn" onclick="S.screen='shoot';render()">← 返回拍照</button>
      <button class="nb-btn" onclick="S.screen='select';render()" style="background:#e8f0f8;color:#1a4a7c;border-color:#b0c8e8">📋 修改選項</button>
    </div>
    <div class="nav-row">
      <button class="nb-btn" onclick="S.screen='project';render()">✏️ 修改案件</button>
      <button class="nb-btn" style="color:#c0392b;border-color:#e8b0b0"
        onclick="if(confirm('重選縣市將清除所有已拍照片,確定嗎?')){S.selectedIds=new Set();S.photos={};S.locations={};S.screen='city';render()}">🔄 重選縣市</button>
    </div>
  </div>`;
  el.innerHTML=h;
}

function goToMissing(){const items=getSelected(),idx=items.findIndex(i=>!S.photos[i.id]);if(idx>=0){S.currentIdx=idx;S.screen="shoot";render();}}

function showPhotoModal(url,name){
  document.getElementById("photoModal")?.remove();
  const m=document.createElement("div");
  m.id="photoModal";
  m.style.cssText="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.93);z-index:9999;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:16px;";
  m.innerHTML=`
    <div style="background:rgba(255,255,255,0.13);border-radius:10px;padding:10px 18px;margin-bottom:14px;text-align:center;color:#fff;font-size:13px;line-height:2">
      📱 <strong>手機</strong>:長按圖片 → 儲存至相簿<br>
      💻 <strong>電腦</strong>:右鍵圖片 → 另存圖片為…
    </div>
    <img src="${url}" style="max-width:100%;max-height:62vh;border-radius:10px;object-fit:contain;display:block;box-shadow:0 4px 24px rgba(0,0,0,0.5)">
    <div style="color:#aaa;font-size:11px;margin-top:10px;font-family:'Courier New',monospace">${name}.jpg</div>
    <button onclick="document.getElementById('photoModal').remove()"
      style="margin-top:18px;padding:11px 32px;background:#fff;border:none;border-radius:8px;font-size:14px;font-weight:700;cursor:pointer;color:#333">✕ 關閉</button>`;
  m.addEventListener("click",e=>{if(e.target===m)m.remove();});
  document.body.appendChild(m);
}

function downloadPhoto(id){
  const p=S.photos[id];if(!p||!p.url)return;
  showPhotoModal(p.url,p.name);
}

async function downloadZip(){
  const items=getSelected();
  const captured=items.filter(i=>S.photos[i.id]);
  if(!captured.length){alert("尚無已拍攝的照片");return;}
  S.downloading=true;render();
  try{
    const zip=new JSZip();
    for(const item of captured){
      const p=S.photos[item.id];
      const folder=p.folder||getFolder(item);
      let blob=p.blob;
      if(!blob){try{const r=await fetch(p.url);blob=await r.blob();}catch(e){continue;}}
      zip.folder(folder).file(p.name+".jpg",blob);
    }
    const content=await zip.generateAsync({type:"blob",compression:"DEFLATE",compressionOptions:{level:6}});
    const a=document.createElement("a");
    a.href=URL.createObjectURL(content);
    a.download=`消防照片_${S.projectName}_${S.inspDate}.zip`;
    a.click();
  }catch(e){alert("下載失敗:"+e.message);}
  S.downloading=false;render();
}

function exportNBList(){
  const items=getSelected();
  let t=`新北市消防照片檔名清單\n案場:${S.projectName}\n地址:${S.projectAddress}\n檢查日期(西元):${S.inspDate}\n監造:${S.supervisorName} 測試:${S.testerName}\n`;
  if(S.caseType)t+=`類別:${S.caseType}\n`;
  t+=`\n${"─".repeat(55)}\n【照片審查順序表對應檔名】\n${"─".repeat(55)}\n\n`;
  items.forEach((item,idx)=>{
    const p=S.photos[item.id],fname=nbFname(item,idx)+".jpg";
    t+=`${String(idx+1).padStart(3,"0")}. [${p?"✓":"✗"}] 資料夾:${getFolder(item)}\n     檔名:${fname}\n`;
    t+=`     設備:${item.category}${item.sub?" ▸ "+item.sub:""}\n`;
    if(S.locations[item.id])t+=`     位置:${S.locations[item.id]}\n`;
    t+="\n";
  });
  t+=`\n備註:★ 既有設備可免附 ◎ 耐燃耐熱鋪設可免附\n`;
  const a=document.createElement("a");
  a.href=URL.createObjectURL(new Blob([t],{type:"text/plain;charset=utf-8"}));
  a.download=`新北市照片清單_${S.projectName}.txt`;a.click();
}

function exportJSON(){
  const items=getSelected();
  const obj={city:S.city==="NB"?"新北市":"台北市",
    project:{name:S.projectName,address:S.projectAddress,inspDate:S.inspDate,
      supervisor:S.supervisorName,tester:S.testerName,builder:S.builderName,...(S.caseType?{caseType:S.caseType}:{})},
    photos:items.map((item,idx)=>{
      const p=S.photos[item.id],fname=S.city==="NB"?nbFname(item,idx):tpFname(item,idx);
      return{seq:idx+1,id:item.id,folder:getFolder(item),category:item.category,...(item.sub?{sub:item.sub}:{}),
        label:item.name,fileName:fname+".jpg",location:S.locations[item.id]||"",
        status:p?"captured":"missing"};
    }),
    summary:{total:items.length,captured:Object.keys(S.photos).length,missing:items.length-Object.keys(S.photos).length}};
  const a=document.createElement("a");
  a.href=URL.createObjectURL(new Blob([JSON.stringify(obj,null,2)],{type:"application/json"}));
  a.download=`照片清單_${S.projectName}.json`;a.click();
}

render();
</script>
</body>
</html>
加入購物車成功!