gateway.html 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
  6. <meta name="color-scheme" content="light">
  7. <title>__TITLE__</title>
  8. <style>
  9. :root{
  10. --ink:#1B2559; --sub:#64748B; --line:#E2E8F0; --card:#FFFFFF;
  11. --bg:#F5F8FF; --accent:#5B5BD6; --accent-soft:#EEF0FF; --accent-line:#D5D9F5;
  12. --accent2:#7C6FF0; --grad:linear-gradient(135deg,#5B5BD6 0%,#7C6FF0 60%,#9BB8F5 100%);
  13. --ok:#1E7A46; --ok-soft:#EAF6EF; --warn:#8A6A1F; --warn-soft:#FAF4E6;
  14. --radius:14px;
  15. }
  16. *{box-sizing:border-box}
  17. html,body{margin:0;padding:0}
  18. body{
  19. background:var(--bg); color:var(--ink); min-height:100vh;
  20. font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
  21. line-height:1.55; -webkit-font-smoothing:antialiased;
  22. padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
  23. }
  24. .wrap{max-width:720px;margin:0 auto;padding:20px 16px 56px}
  25. header{padding:8px 0 4px}
  26. .eyebrow{color:var(--accent);font-size:12px;font-weight:700;letter-spacing:.14em;text-transform:uppercase}
  27. h1{font-size:22px;margin:6px 0 2px;line-height:1.3;word-break:break-word}
  28. .sub{color:var(--sub);font-size:13.5px;margin:0 0 4px}
  29. .card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:16px;margin-top:14px;box-shadow:0 1px 2px rgba(26,22,20,.03)}
  30. .limit{background:var(--warn-soft);border:1px solid #EFE1BE;border-radius:var(--radius);padding:14px 16px;margin-top:14px}
  31. .limit .big{font-weight:800;font-size:15px}
  32. .limit .channel{color:var(--accent);font-weight:700}
  33. .limit p{margin:6px 0 0;font-size:13px;color:var(--sub)}
  34. .notice{display:flex;gap:10px;align-items:flex-start;background:var(--accent-soft);border:1px solid var(--accent-line);border-radius:12px;padding:12px 14px;margin-top:12px;font-size:13px;color:#7A1220}
  35. .notice .ico{flex:0 0 auto;font-weight:800}
  36. #drop{
  37. margin-top:14px;border:2px dashed var(--accent-line);border-radius:var(--radius);
  38. background:linear-gradient(180deg,#fff, #FFF9F9); padding:30px 18px; text-align:center;
  39. cursor:pointer; transition:border-color .15s, background .15s;
  40. }
  41. #drop.hot{border-color:var(--accent);background:var(--accent-soft)}
  42. #drop .big{font-size:16px;font-weight:700;margin-bottom:6px}
  43. #drop .small{font-size:12.5px;color:var(--sub)}
  44. #drop .pick{display:inline-block;margin-top:12px;background:var(--accent);color:#fff;font-weight:700;
  45. border:0;border-radius:10px;padding:10px 20px;font-size:14.5px;cursor:pointer}
  46. #drop .pick:active{transform:translateY(1px)}
  47. .files{margin-top:16px;display:flex;flex-direction:column;gap:10px}
  48. .f{border:1px solid var(--line);border-radius:12px;padding:12px 14px;background:#fff}
  49. .f .row{display:flex;justify-content:space-between;gap:10px;align-items:baseline}
  50. .f .name{font-weight:600;font-size:13.5px;word-break:break-all}
  51. .f .meta{color:var(--sub);font-size:12px;white-space:nowrap}
  52. .f .slot{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--sub);margin-top:2px;word-break:break-all}
  53. .bar{height:7px;border-radius:99px;background:#EFEAE3;overflow:hidden;margin-top:9px}
  54. .bar > i{display:block;height:100%;width:0;background:var(--accent);transition:width .2s}
  55. .f.done .bar > i{background:var(--ok)}
  56. .f.done{border-color:#CDE7D7;background:var(--ok-soft)}
  57. .f.err .bar > i{background:#B8182A}
  58. .status{font-size:12px;margin-top:6px;font-weight:600}
  59. .status.ok{color:var(--ok)} .status.err{color:var(--accent)} .status.up{color:var(--sub)}
  60. footer{color:var(--sub);font-size:11.5px;margin-top:22px;text-align:center;line-height:1.7}
  61. code{background:#F2EDE6;border-radius:5px;padding:1px 5px;font-size:12px}
  62. .hide{display:none}
  63. </style>
  64. </head>
  65. <body>
  66. <div class="wrap">
  67. <header>
  68. <div class="brand" style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
  69. <span style="width:26px;height:26px;border-radius:8px;background:var(--grad);display:inline-flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:14px">F</span>
  70. <span style="font-weight:800;font-size:13px;color:var(--accent)">未来飞马 FmodeAgent</span>
  71. <span style="font-size:11px;color:var(--sub)">大文件接收网关</span>
  72. </div>
  73. <h1>__TITLE__</h1>
  74. <p class="sub">拖入文件即直传对象存储,不占聊天通道 —— 多大都行。</p>
  75. </header>
  76. <div class="limit">
  77. <div class="big">当前通道限制:<span class="channel">__CHANNEL_NAME__</span></div>
  78. <div class="big" style="margin-top:2px">上限 __LIMIT_TEXT__</div>
  79. <p>__LIMIT_NOTE__</p>
  80. </div>
  81. <div class="notice"><span class="ico">!</span><span>这是通道限制,不是你操作的问题。用本页直传存储,原文件多大都可以。</span></div>
  82. <div id="drop">
  83. <div class="big">拖拽文件到此处,或点击选择</div>
  84. <div class="small">支持多文件同时上传 · 手机、电脑均可</div>
  85. <button class="pick" id="pick" type="button">选择文件</button>
  86. <input id="input" type="file" multiple class="hide">
  87. </div>
  88. <div class="files" id="files"></div>
  89. <footer>
  90. <div>上传地址由临时授权(STS)签发,约 __TTL_HOURS__ 小时内有效。</div>
  91. <div>本页不含任何密钥;每个文件写入独立槽位,互不影响。</div>
  92. <div id="slotsleft"></div>
  93. <div style="margin-top:12px;padding-top:10px;border-top:1px solid var(--line);color:#94A3B8;font-size:11px">由 <b style="color:var(--accent)">未来飞马 FmodeAgent</b> 提供技术支持 · fmode.cn</div>
  94. </footer>
  95. </div>
  96. <script id="gateway-config" type="application/json">__CONFIG_JSON__</script>
  97. <script>
  98. (function(){
  99. var CFG = JSON.parse(document.getElementById('gateway-config').textContent);
  100. var PUB_BASE = CFG.publicBase, SLOTS = CFG.slots, TTL = CFG.ttlHours;
  101. var drop = document.getElementById('drop'), input = document.getElementById('input'),
  102. pick = document.getElementById('pick'), filesEl = document.getElementById('files');
  103. var free = SLOTS.slice();
  104. function fmt(n){ if(n<1024) return n+' B'; var u=['KB','MB','GB'], i=-1; do{ n/=1024; i++; }while(n>=1024&&i<2); return n.toFixed(n<10?1:0)+' '+u[i]; }
  105. function esc(s){ return String(s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
  106. pick.addEventListener('click', function(e){ e.stopPropagation(); input.click(); });
  107. drop.addEventListener('click', function(){ input.click(); });
  108. input.addEventListener('change', function(){ enqueue(input.files); input.value=''; });
  109. ['dragenter','dragover'].forEach(function(ev){ drop.addEventListener(ev, function(e){ e.preventDefault(); drop.classList.add('hot'); }); });
  110. ['dragleave','drop'].forEach(function(ev){ drop.addEventListener(ev, function(e){ e.preventDefault(); drop.classList.remove('hot'); }); });
  111. drop.addEventListener('drop', function(e){ if(e.dataTransfer && e.dataTransfer.files) enqueue(e.dataTransfer.files); });
  112. function updateLeft(){ var el=document.getElementById('slotsleft'); el.textContent = '剩余可用槽位 '+free.length+' / '+SLOTS.length; }
  113. updateLeft();
  114. function enqueue(fl){
  115. for(var i=0;i<fl.length;i++){ (function(f){ uploadOne(f); })(fl[i]); }
  116. }
  117. function uploadOne(file){
  118. var slot = free.shift();
  119. var row = document.createElement('div');
  120. row.className='f';
  121. row.innerHTML = '<div class="row"><span class="name">'+esc(file.name)+'</span><span class="meta">'+fmt(file.size)+'</span></div>'
  122. + '<div class="slot">'+(slot? '槽位 '+slot.n+' · '+esc(slot.name):'槽位已用尽')+'</div>'
  123. + '<div class="bar"><i></i></div><div class="status up">'+ (slot? '等待上传…' : '无可用槽位,请重新生成页面') +'</div>';
  124. filesEl.appendChild(row);
  125. updateLeft();
  126. if(!slot){ row.classList.add('err'); row.querySelector('.status').className='status err'; return; }
  127. var bar = row.querySelector('.bar > i'), st = row.querySelector('.status');
  128. var xhr = new XMLHttpRequest();
  129. xhr.open('PUT', slot.url, true);
  130. xhr.setRequestHeader('Content-Type', slot.contentType || 'application/octet-stream');
  131. xhr.setRequestHeader('x-obs-acl', 'public-read');
  132. st.textContent='上传中…'; st.className='status up';
  133. xhr.upload.onprogress = function(e){ if(e.lengthComputable){ var p=Math.round(e.loaded/e.total*100); bar.style.width=p+'%'; st.textContent='上传中… '+p+'%'; } };
  134. xhr.onload = function(){
  135. if(xhr.status>=200 && xhr.status<300){
  136. bar.style.width='100%'; row.classList.add('done'); st.className='status ok';
  137. st.innerHTML = '上传成功 ✓ <a href="'+slot.publicUrl+'" target="_blank" rel="noopener">查看/下载</a>';
  138. } else {
  139. row.classList.add('err'); st.className='status err';
  140. st.textContent='上传失败(HTTP '+xhr.status+')。若本页已打开超过 '+TTL+' 小时,请让智能体重新生成。';
  141. }
  142. };
  143. xhr.onerror = function(){ row.classList.add('err'); st.className='status err'; st.textContent='网络中断,请重试或让智能体重新生成页面。'; };
  144. xhr.send(file);
  145. }
  146. })();
  147. </script>
  148. </body>
  149. </html>