diff --git a/backend/internal/handler/http.go b/backend/internal/handler/http.go
index 6f32841..bab078c 100644
--- a/backend/internal/handler/http.go
+++ b/backend/internal/handler/http.go
@@ -2,7 +2,11 @@ package handler
import (
"context"
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
"net/http"
+ "net/netip"
"net/url"
"path/filepath"
"strings"
@@ -118,6 +122,14 @@ func (h *HTTPHandler) registerDevice(c *gin.Context) {
return
}
+ networkGroupKey, publicIPHash := deriveClientNetworkIdentity(c.ClientIP())
+ if networkGroupKey != "" {
+ input.NetworkGroupKey = networkGroupKey
+ }
+ if publicIPHash != "" {
+ input.PublicIPHash = publicIPHash
+ }
+
device, session := h.deps.DeviceService.Register(input, c.Request.UserAgent(), c.GetHeader("X-Device-Token"))
c.JSON(http.StatusOK, gin.H{"data": gin.H{
"id": device.ID,
@@ -611,6 +623,46 @@ func decodeDownloadFilename(filename string) string {
return strings.TrimSpace(decoded)
}
+func deriveClientNetworkIdentity(clientIP string) (string, string) {
+ addr, err := netip.ParseAddr(strings.TrimSpace(clientIP))
+ if err != nil {
+ return "", ""
+ }
+
+ addr = addr.Unmap()
+ hash := hashIP(addr.String())
+
+ if addr.IsLoopback() {
+ return "loopback", hash
+ }
+
+ if addr.Is4() {
+ bytes := addr.As4()
+ switch {
+ case bytes[0] == 10 || bytes[0] == 127:
+ return fmt.Sprintf("v4:%d.%d.%d", bytes[0], bytes[1], bytes[2]), hash
+ case bytes[0] == 192 && bytes[1] == 168:
+ return fmt.Sprintf("v4:%d.%d.%d", bytes[0], bytes[1], bytes[2]), hash
+ case bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31:
+ return fmt.Sprintf("v4:%d.%d.%d", bytes[0], bytes[1], bytes[2]), hash
+ default:
+ return "pub4:" + hash[:16], hash
+ }
+ }
+
+ prefix := netip.PrefixFrom(addr, 64).Masked()
+ if addr.IsPrivate() {
+ return "v6:" + prefix.Addr().String(), hash
+ }
+
+ return "pub6:" + hash[:16], hash
+}
+
+func hashIP(value string) string {
+ sum := sha256.Sum256([]byte(strings.TrimSpace(value)))
+ return hex.EncodeToString(sum[:])
+}
+
func (h *HTTPHandler) ensureFallbackBucket(ctx context.Context, transferID string) error {
if h.deps.MinIOClient == nil {
return nil
diff --git a/backend/internal/handler/http_test.go b/backend/internal/handler/http_test.go
index aa9e6ad..90f6c08 100644
--- a/backend/internal/handler/http_test.go
+++ b/backend/internal/handler/http_test.go
@@ -134,6 +134,46 @@ func TestProtectedRoutesAcceptDeviceCredentialsFromCookies(t *testing.T) {
}
}
+func TestRegisterDeviceDerivesNetworkGroupFromClientIP(t *testing.T) {
+ router, _ := newTestRouter()
+
+ alpha := registerDeviceWithRemoteAddr(t, router, map[string]any{
+ "device_id": "alpha",
+ "name": "Alpha",
+ "type": "desktop",
+ }, "192.168.1.10:1234")
+ bravo := registerDeviceWithRemoteAddr(t, router, map[string]any{
+ "device_id": "bravo",
+ "name": "Bravo",
+ "type": "phone",
+ }, "192.168.1.77:4567")
+ _ = registerDeviceWithRemoteAddr(t, router, map[string]any{
+ "device_id": "charlie",
+ "name": "Charlie",
+ "type": "phone",
+ }, "10.0.0.8:9999")
+
+ req := httptest.NewRequest(http.MethodGet, "/api/devices/candidates?deviceId="+alpha.ID, nil)
+ req.Header.Set("X-Device-ID", alpha.ID)
+ req.Header.Set("X-Device-Token", alpha.AuthToken)
+ resp := httptest.NewRecorder()
+ router.ServeHTTP(resp, req)
+
+ if resp.Code != http.StatusOK {
+ t.Fatalf("expected candidates request 200, got %d: %s", resp.Code, resp.Body.String())
+ }
+
+ var payload struct {
+ Data []registeredDevice `json:"data"`
+ }
+ if err := json.Unmarshal(resp.Body.Bytes(), &payload); err != nil {
+ t.Fatalf("decode candidates response: %v", err)
+ }
+ if len(payload.Data) != 1 || payload.Data[0].ID != bravo.ID {
+ t.Fatalf("expected only same-network device %q, got %+v", bravo.ID, payload.Data)
+ }
+}
+
func newTestRouter() (http.Handler, *store.MemoryStore) {
memStore := store.NewMemoryStore(model.RuntimeConfig{})
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
@@ -183,6 +223,33 @@ func registerDevice(t *testing.T, router http.Handler, payload map[string]any) r
return payloadWrapper.Data
}
+func registerDeviceWithRemoteAddr(t *testing.T, router http.Handler, payload map[string]any, remoteAddr string) registeredDevice {
+ t.Helper()
+ body, err := json.Marshal(payload)
+ if err != nil {
+ t.Fatalf("marshal register body: %v", err)
+ }
+
+ req := httptest.NewRequest(http.MethodPost, "/api/devices/register", bytes.NewReader(body))
+ req.Header.Set("Content-Type", "application/json")
+ req.RemoteAddr = remoteAddr
+ resp := httptest.NewRecorder()
+ router.ServeHTTP(resp, req)
+
+ if resp.Code != http.StatusOK {
+ t.Fatalf("expected register 200, got %d: %s", resp.Code, resp.Body.String())
+ }
+
+ var payloadWrapper struct {
+ Data registeredDevice `json:"data"`
+ }
+ if err := json.Unmarshal(resp.Body.Bytes(), &payloadWrapper); err != nil {
+ t.Fatalf("decode register response: %v", err)
+ }
+
+ return payloadWrapper.Data
+}
+
func createTransfer(t *testing.T, router http.Handler, device registeredDevice, payload map[string]any) transferRecord {
t.Helper()
body, err := json.Marshal(payload)
diff --git a/frontend/dist/assets/index-BjlDNx75.js b/frontend/dist/assets/index-Dvss27fc.js
similarity index 70%
rename from frontend/dist/assets/index-BjlDNx75.js
rename to frontend/dist/assets/index-Dvss27fc.js
index 98ebeeb..c82b882 100644
--- a/frontend/dist/assets/index-BjlDNx75.js
+++ b/frontend/dist/assets/index-Dvss27fc.js
@@ -14,4 +14,4 @@
* @vue/runtime-dom v3.5.30
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
-**/let Ns;const Vr=typeof window<"u"&&window.trustedTypes;if(Vr)try{Ns=Vr.createPolicy("vue",{createHTML:e=>e})}catch{}const _o=Ns?e=>Ns.createHTML(e):e=>e,Qa="http://www.w3.org/2000/svg",ec="http://www.w3.org/1998/Math/MathML",ct=typeof document<"u"?document:null,qr=ct&&ct.createElement("template"),tc={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?ct.createElementNS(Qa,e):t==="mathml"?ct.createElementNS(ec,e):n?ct.createElement(e,{is:n}):ct.createElement(e);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>ct.createTextNode(e),createComment:e=>ct.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>ct.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,o){const a=n?n.previousSibling:t.lastChild;if(r&&(r===o||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===o||!(r=r.nextSibling)););else{qr.innerHTML=_o(s==="svg"?``:s==="mathml"?``:e);const c=qr.content;if(s==="svg"||s==="mathml"){const f=c.firstChild;for(;f.firstChild;)c.appendChild(f.firstChild);c.removeChild(f)}t.insertBefore(c,n)}return[a?a.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},nc=Symbol("_vtc");function sc(e,t,n){const s=e[nc];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const Gr=Symbol("_vod"),rc=Symbol("_vsh"),ic=Symbol(""),oc=/(?:^|;)\s*display\s*:/;function lc(e,t,n){const s=e.style,r=ae(n);let o=!1;if(n&&!r){if(t)if(ae(t))for(const a of t.split(";")){const c=a.slice(0,a.indexOf(":")).trim();n[c]==null&&zn(s,c,"")}else for(const a in t)n[a]==null&&zn(s,a,"");for(const a in n)a==="display"&&(o=!0),zn(s,a,n[a])}else if(r){if(t!==n){const a=s[ic];a&&(n+=";"+a),s.cssText=n,o=oc.test(n)}}else t&&e.removeAttribute("style");Gr in e&&(e[Gr]=o?s.display:"",e[rc]&&(s.display="none"))}const Jr=/\s*!important$/;function zn(e,t,n){if(H(n))n.forEach(s=>zn(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=ac(e,t);Jr.test(n)?e.setProperty(Ct(s),n.replace(Jr,""),"important"):e[s]=n}}const Yr=["Webkit","Moz","ms"],ws={};function ac(e,t){const n=ws[t];if(n)return n;let s=Ae(t);if(s!=="filter"&&s in e)return ws[t]=s;s=Qn(s);for(let r=0;rxs||(dc.then(()=>xs=0),xs=Date.now());function hc(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;nt(mc(s,n.value),t,5,[s])};return n.value=e,n.attached=pc(),n}function mc(e,t){if(H(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>r=>!r._stopped&&s&&s(r))}else return t}const ni=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,gc=(e,t,n,s,r,o)=>{const a=r==="svg";t==="class"?sc(e,s,a):t==="style"?lc(e,n,s):Xn(t)?Us(t)||uc(e,t,n,s,o):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):vc(e,t,s,a))?(Qr(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Zr(e,t,s,a,o,t!=="value")):e._isVueCE&&(yc(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!ae(s)))?Qr(e,Ae(t),s,o,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),Zr(e,t,s,a))};function vc(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&ni(t)&&K(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return ni(t)&&ae(n)?!1:t in e}function yc(e,t){const n=e._def.props;if(!n)return!1;const s=Ae(t);return Array.isArray(n)?n.some(r=>Ae(r)===s):Object.keys(n).some(r=>Ae(r)===s)}const si=e=>{const t=e.props["onUpdate:modelValue"]||!1;return H(t)?n=>jn(t,n):t};function _c(e){e.target.composing=!0}function ri(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const Is=Symbol("_assign");function ii(e,t,n){return t&&(e=e.trim()),n&&(e=js(e)),e}const bc={created(e,{modifiers:{lazy:t,trim:n,number:s}},r){e[Is]=si(r);const o=s||r.props&&r.props.type==="number";Wt(e,t?"change":"input",a=>{a.target.composing||e[Is](ii(e.value,n,o))}),(n||o)&&Wt(e,"change",()=>{e.value=ii(e.value,n,o)}),t||(Wt(e,"compositionstart",_c),Wt(e,"compositionend",ri),Wt(e,"change",ri))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:s,trim:r,number:o}},a){if(e[Is]=si(a),e.composing)return;const c=(o||e.type==="number")&&!/^0\d/.test(e.value)?js(e.value):e.value,f=t??"";c!==f&&(document.activeElement===e&&e.type!=="range"&&(s&&t===n||r&&e.value.trim()===f)||(e.value=f))}},wc=["ctrl","shift","alt","meta"],xc={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>wc.some(n=>e[`${n}Key`]&&!t.includes(n))},Pn=(e,t)=>{if(!e)return e;const n=e._withMods||(e._withMods={}),s=t.join(".");return n[s]||(n[s]=(r,...o)=>{for(let a=0;a{const n=e._withKeys||(e._withKeys={}),s=t.join(".");return n[s]||(n[s]=r=>{if(!("key"in r))return;const o=Ct(r.key);if(t.some(a=>a===o||Ic[a]===o))return e(r)})},Sc=Ce({patchProp:gc},tc);let oi;function Cc(){return oi||(oi=Pa(Sc))}const Tc=(...e)=>{const t=Cc().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=kc(s);if(!r)return;const o=t._component;!K(o)&&!o.render&&!o.template&&(o.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const a=n(r,!1,$c(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),a},t};function $c(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function kc(e){return ae(e)?document.querySelector(e):e}const Mc={class:"footer"},Rc={__name:"AppFooter",emits:["request-admin"],setup(e,{emit:t}){const n=t,s=oe(0);let r=null;Qs(()=>{r&&window.clearTimeout(r)});function o(){s.value+=1,s.value===1&&(r=window.setTimeout(()=>{s.value=0,r=null},2e3)),s.value>=5&&(r&&(window.clearTimeout(r),r=null),s.value=0,n("request-admin"))}return(a,c)=>(j(),V("div",Mc,[y("div",null,[c[0]||(c[0]=nn(" © 2026 AirShare Pro. All rights reserved. ",-1)),c[1]||(c[1]=y("span",{class:"divider-line"},"|",-1)),y("span",{id:"admin-trigger",title:"点击 5 次进入后台",onClick:o},"V 1.0.0")]),c[2]||(c[2]=y("div",{style:{"font-size":"12px","margin-top":"4px"}},[y("a",{href:"https://beian.miit.gov.cn/",target:"_blank",rel:"noreferrer"}," 粤ICP备2026888888号-1 ")],-1))]))}},Ac=["fill","stroke"],pe={__name:"LocalIcon",props:{name:{type:String,required:!0},size:{type:[Number,String],default:24}},setup(e){const t=e,n={light_mode:{type:"stroke",shapes:[{tag:"circle",attrs:{cx:"12",cy:"12",r:"4"}},{tag:"path",attrs:{d:"M12 2v2.2"}},{tag:"path",attrs:{d:"M12 19.8V22"}},{tag:"path",attrs:{d:"M4.93 4.93 6.5 6.5"}},{tag:"path",attrs:{d:"m17.5 17.5 1.57 1.57"}},{tag:"path",attrs:{d:"M2 12h2.2"}},{tag:"path",attrs:{d:"M19.8 12H22"}},{tag:"path",attrs:{d:"m4.93 19.07 1.57-1.57"}},{tag:"path",attrs:{d:"M17.5 6.5 19.07 4.93"}}]},dark_mode:{type:"fill",shapes:[{tag:"path",attrs:{d:"M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z"}}]},add_circle:{type:"stroke",shapes:[{tag:"circle",attrs:{cx:"12",cy:"12",r:"9"}},{tag:"path",attrs:{d:"M12 8v8"}},{tag:"path",attrs:{d:"M8 12h8"}}]},sensors:{type:"stroke",shapes:[{tag:"path",attrs:{d:"M12 12h.01"}},{tag:"path",attrs:{d:"M9.2 14.8a4 4 0 0 1 0-5.6"}},{tag:"path",attrs:{d:"M14.8 9.2a4 4 0 0 1 0 5.6"}},{tag:"path",attrs:{d:"M6.4 17.6a8 8 0 0 1 0-11.2"}},{tag:"path",attrs:{d:"M17.6 6.4a8 8 0 0 1 0 11.2"}}]},smartphone:{type:"stroke",shapes:[{tag:"rect",attrs:{x:"7",y:"2.5",width:"10",height:"19",rx:"2.5"}},{tag:"path",attrs:{d:"M10 5h4"}},{tag:"circle",attrs:{cx:"12",cy:"18",r:"0.8"}}]},laptop_mac:{type:"stroke",shapes:[{tag:"rect",attrs:{x:"5",y:"4",width:"14",height:"10",rx:"1.5"}},{tag:"path",attrs:{d:"M3 18h18"}},{tag:"path",attrs:{d:"M8 18a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2"}}]},close:{type:"stroke",shapes:[{tag:"path",attrs:{d:"M6 6l12 12"}},{tag:"path",attrs:{d:"M18 6 6 18"}}]},cloud_upload:{type:"stroke",shapes:[{tag:"path",attrs:{d:"M7 18a4 4 0 1 1 .7-7.94A5.5 5.5 0 0 1 18 11a3.5 3.5 0 1 1-.5 7"}},{tag:"path",attrs:{d:"M12 10v8"}},{tag:"path",attrs:{d:"m8.8 13.2 3.2-3.2 3.2 3.2"}}]},arrow_upward:{type:"stroke",shapes:[{tag:"path",attrs:{d:"M12 19V6"}},{tag:"path",attrs:{d:"m6.5 11.5 5.5-5.5 5.5 5.5"}}]},send_and_archive:{type:"stroke",shapes:[{tag:"path",attrs:{d:"M3 6h18l-2 4H5Z"}},{tag:"path",attrs:{d:"M5 10v8a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-8"}},{tag:"path",attrs:{d:"M12 11v5"}},{tag:"path",attrs:{d:"m9.5 13.5 2.5 2.5 2.5-2.5"}}]},chat_bubble:{type:"stroke",shapes:[{tag:"path",attrs:{d:"M6 18.5 3.5 21v-5A7.5 7.5 0 0 1 11 4.5h2A7.5 7.5 0 0 1 20.5 12v.5A7.5 7.5 0 0 1 13 20H8.5"}}]},content_copy:{type:"stroke",shapes:[{tag:"rect",attrs:{x:"9",y:"9",width:"10",height:"10",rx:"2"}},{tag:"path",attrs:{d:"M7 15H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v1"}}]},check:{type:"stroke",shapes:[{tag:"path",attrs:{d:"m5 12 4.2 4.2L19 7.5"}}]},draft:{type:"stroke",shapes:[{tag:"rect",attrs:{x:"4",y:"5",width:"16",height:"14",rx:"2"}},{tag:"path",attrs:{d:"m5 7 7 5 7-5"}}]},save:{type:"stroke",shapes:[{tag:"path",attrs:{d:"M5 20h14a1 1 0 0 0 1-1V7.5L16.5 4H5a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1Z"}},{tag:"path",attrs:{d:"M8 4v5h7"}},{tag:"rect",attrs:{x:"8",y:"14",width:"8",height:"4",rx:"1"}}]},download:{type:"stroke",shapes:[{tag:"path",attrs:{d:"M12 5v10"}},{tag:"path",attrs:{d:"m7.5 10.5 4.5 4.5 4.5-4.5"}},{tag:"path",attrs:{d:"M5 19h14"}}]}},s=It(()=>n[t.name]||n.close),r=It(()=>typeof t.size=="number"?`${t.size}px`:/^\d+(\.\d+)?$/.test(t.size)?`${t.size}px`:t.size);return(o,a)=>(j(),V("span",{class:"app-icon",style:Le({width:r.value,height:r.value}),"aria-hidden":"true"},[(j(),V("svg",{viewBox:"0 0 24 24",fill:s.value.type==="fill"?"currentColor":"none",stroke:s.value.type==="stroke"?"currentColor":"none","stroke-width":"1.8","stroke-linecap":"round","stroke-linejoin":"round"},[(j(!0),V(fe,null,en(s.value.shapes,(c,f)=>(j(),vt(ua(c.tag),go({key:`${e.name}-${f}`},{ref_for:!0},c.attrs),null,16))),128))],8,Ac))],4))}},Ec={class:"header"},Oc={__name:"AppHeader",props:{theme:{type:String,required:!0}},emits:["toggle-theme"],setup(e){return(t,n)=>(j(),V("div",Ec,[n[1]||(n[1]=y("h1",null,"AirShare Pro",-1)),n[2]||(n[2]=y("p",null,"跨端局域网 & P2P 传输中心",-1)),y("button",{class:"theme-toggle",title:"切换日夜模式",onClick:n[0]||(n[0]=s=>t.$emit("toggle-theme"))},[q(pe,{id:"theme-icon",name:e.theme==="dark"?"dark_mode":"light_mode",size:"22"},null,8,["name"])])]))}},Pc={class:"card"},Dc={key:0,class:"section-title"},mn={__name:"GlassCard",props:{title:{type:String,default:""}},setup(e){return(t,n)=>(j(),V("div",Pc,[e.title?(j(),V("div",Dc,te(e.title),1)):Be("",!0),da(t.$slots,"default")]))}},Nc={class:"admin-panel active"},Fc={class:"card admin-header-card"},Uc={class:"transfer-head transfer-head-compact"},Lc={class:"main-grid admin-summary-grid"},Bc={class:"admin-stats-panel"},jc={class:"admin-stats-row"},Hc={class:"admin-fluid-content"},Kc={class:"admin-fluid-icon"},zc={class:"admin-fluid-copy"},Wc={key:0,class:"stat-suffix"},Vc={class:"admin-config-stack"},qc={class:"text-input-group admin-config-row admin-config-row-field admin-config-row-last"},Gc={class:"admin-field-control-row"},Jc=["value"],Yc={class:"text-input-group admin-config-row admin-config-row-field admin-config-row-last"},Xc={class:"admin-field-control-row"},Zc=["value"],Qc={class:"admin-config-insights"},eu={class:"admin-config-highlight"},tu={class:"admin-config-highlight"},nu={class:"admin-table-wrapper"},su={class:"admin-table"},ru={class:"admin-record-type-cell"},iu=["title"],ou={__name:"AdminPanel",props:{stats:{type:Array,required:!0},records:{type:Array,required:!0},fileLimit:{type:Number,required:!0},minioCapacity:{type:Number,required:!0}},emits:["exit","save-config","update:file-limit","update:minio-capacity"],setup(e){function t(a){const c=Number(a)||0;return c>=1024?`${(c/1024).toFixed(c%1024===0?0:1)} GB`:`${c} MB`}function n(a){const c=Number(a)||0;return c>=1024?`${(c/1024).toFixed(c%1024===0?0:1)} TB`:`${c} GB`}function s(a){return a==="blue"?{color:"var(--accent-blue)"}:a==="cyan"?{color:"var(--accent-cyan)"}:a==="success"?{color:"var(--success-green)"}:a==="danger"?{color:"var(--danger-red)"}:{color:"var(--text-main)"}}function r(a){return a==="success"?{color:"var(--success-green)",fontWeight:500}:a==="primary"?{color:"var(--accent-blue)",fontWeight:500}:{color:"var(--danger-red)",fontWeight:500}}function o(a){const c=Number(a)||0;return{"--fluid-level":`${Math.max(0,Math.min(c,100))}%`}}return(a,c)=>(j(),V("div",Nc,[y("div",Fc,[y("div",Uc,[c[5]||(c[5]=y("div",{class:"connected-to"},[y("h2",{class:"admin-title"},"管理控制台"),y("p",{class:"admin-subtitle"},"AirShare Pro System Dashboard")],-1)),y("button",{class:"btn-small-primary",type:"button",onClick:c[0]||(c[0]=f=>a.$emit("exit"))},"退出管理")])]),y("div",Lc,[q(mn,{class:"admin-stats-card",title:"系统运行状态"},{default:Yt(()=>[y("div",Bc,[y("div",jc,[(j(!0),V(fe,null,en(e.stats,f=>(j(),V("div",{key:f.label,class:St(["admin-stat-item",{"admin-stat-item-fluid":f.kind==="minio"}])},[f.kind==="minio"?(j(),V("div",{key:0,class:"admin-fluid-card",style:Le(o(f.percent))},[c[6]||(c[6]=y("div",{class:"admin-fluid-fill"},[y("span",{class:"admin-fluid-wave admin-fluid-wave-a"}),y("span",{class:"admin-fluid-wave admin-fluid-wave-b"})],-1)),y("div",Hc,[y("div",Kc,[q(pe,{name:"save",size:"18"})]),y("div",zc,[y("h3",{style:Le(s(f.tone))},te(f.value),5),y("p",null,te(f.label),1),y("small",null,te(f.detail),1)])])],4)):(j(),V(fe,{key:1},[c[7]||(c[7]=y("span",{class:"admin-stat-kicker"},"实时指标",-1)),y("h3",{style:Le(s(f.tone))},[nn(te(f.value),1),f.suffix?(j(),V("span",Wc,te(f.suffix),1)):Be("",!0)],4),y("p",null,te(f.label),1)],64))],2))),128))])])]),_:1}),q(mn,{class:"admin-config-card",title:"核心参数配置"},{default:Yt(()=>[y("div",Vc,[y("div",qc,[c[8]||(c[8]=y("div",{class:"admin-field-meta"},[y("label",{class:"admin-field-label",for:"admin-file-limit"},"单文件大小限制"),y("p",{class:"admin-field-hint"},"单位为 MB,超过该阈值的文件会按当前后端策略处理。")],-1)),y("div",Gc,[y("input",{id:"admin-file-limit",value:e.fileLimit,min:"1",placeholder:"10240",type:"number",onInput:c[1]||(c[1]=f=>a.$emit("update:file-limit",Number(f.target.value)||0))},null,40,Jc),y("button",{title:"保存配置",type:"button",onClick:c[2]||(c[2]=f=>a.$emit("save-config"))},[q(pe,{name:"save",size:"18"})])])]),y("div",Yc,[c[9]||(c[9]=y("div",{class:"admin-field-meta"},[y("label",{class:"admin-field-label",for:"admin-minio-capacity"},"MinIO 总容量"),y("p",{class:"admin-field-hint"},"单位为 GB,用于容量卡和液位比例计算。")],-1)),y("div",Xc,[y("input",{id:"admin-minio-capacity",value:e.minioCapacity,min:"1",placeholder:"120",type:"number",onInput:c[3]||(c[3]=f=>a.$emit("update:minio-capacity",Number(f.target.value)||0))},null,40,Zc),y("button",{title:"保存配置",type:"button",onClick:c[4]||(c[4]=f=>a.$emit("save-config"))},[q(pe,{name:"save",size:"18"})])])]),y("div",Qc,[y("div",eu,[c[10]||(c[10]=y("span",{class:"admin-config-badge"},"ACTIVE POLICY",-1)),y("h3",null,te(t(e.fileLimit)),1),c[11]||(c[11]=y("p",null,"当前单文件阈值。超过该体积后,文件会按后端已设定的传输与存档策略处理。",-1))]),y("div",tu,[c[12]||(c[12]=y("span",{class:"admin-config-badge"},"MINIO CAPACITY",-1)),y("h3",null,te(n(e.minioCapacity)),1),c[13]||(c[13]=y("p",null,"当前 MinIO 总容量基线,用于后台容量展示与液位占比计算。",-1))])])])]),_:1})]),q(mn,{class:"admin-table-card",title:"最近传输记录(Top 5)"},{default:Yt(()=>[y("div",nu,[y("table",su,[c[14]||(c[14]=y("thead",null,[y("tr",null,[y("th",null,"时间"),y("th",null,"发送端特征"),y("th",null,"传输类型"),y("th",null,"数据量"),y("th",null,"状态")])],-1)),y("tbody",null,[(j(!0),V(fe,null,en(e.records,f=>(j(),V("tr",{key:`${f.time}-${f.peer}`},[y("td",null,te(f.time),1),y("td",null,te(f.peer),1),y("td",ru,[y("span",{class:"admin-record-type",title:f.type},te(f.type),9,iu)]),y("td",null,te(f.size),1),y("td",{style:Le(r(f.tone))},te(f.status),5)]))),128))])])])]),_:1})]))}},lu={class:"local-device-name"},au={key:0,class:"radar-container"},cu={class:"radar"},uu={key:1,class:"device-list"},fu=["onClick"],du={class:"device-icon"},pu={class:"device-info"},hu={key:2,class:"radar-container"},mu={class:"radar"},gu={__name:"DeviceDiscoveryCard",props:{isScanning:{type:Boolean,required:!0},localDeviceName:{type:String,default:""},devices:{type:Array,required:!0}},emits:["select-device"],setup(e,{emit:t}){const n=t;function s(r){n("select-device",r)}return(r,o)=>(j(),vt(mn,{title:"局域网自动发现"},{default:Yt(()=>[y("p",lu,[o[0]||(o[0]=nn(" 本机:",-1)),y("strong",null,te(e.localDeviceName||"识别中"),1)]),e.isScanning?(j(),V("div",au,[y("div",cu,[q(pe,{class:"radar-icon",name:"sensors",size:"36"})]),o[1]||(o[1]=y("p",{class:"scan-status"},"正在扫描附近设备...",-1))])):e.devices.length?(j(),V("div",uu,[(j(!0),V(fe,null,en(e.devices,a=>(j(),V("button",{key:a.id,class:"device-item",type:"button",onClick:c=>s(a)},[y("div",du,[q(pe,{name:a.icon,size:"24"},null,8,["name"])]),y("div",pu,[y("h4",null,te(a.name),1),y("p",null,te(a.description),1)]),o[2]||(o[2]=y("div",{class:"device-status-beacon","aria-hidden":"true"},[y("span",{class:"device-status-dot"}),y("span",{class:"device-status-ring"}),y("span",{class:"device-status-ring device-status-ring-delay"})],-1))],8,fu))),128))])):(j(),V("div",hu,[y("div",mu,[q(pe,{class:"radar-icon",name:"devices",size:"36"})]),o[3]||(o[3]=y("p",{class:"scan-status"},"暂未发现局域网设备,请保持页面开启后重试",-1))]))]),_:1}))}},vu={key:0,class:"room-action-area"},yu={class:"room-input-group"},_u=["value"],bu={key:0,class:"pending-downloads"},wu={class:"pending-downloads-head"},xu=["href"],Iu={class:"pending-download-copy"},Su=["title"],Cu={class:"pending-download-icon","aria-hidden":"true"},Tu={key:1,class:"waiting-area"},$u={class:"huge-code"},ku={__name:"RemoteRoomCard",props:{roomCodeInput:{type:String,required:!0},isWaiting:{type:Boolean,required:!0},generatedCode:{type:String,required:!0},pendingDownloads:{type:Array,required:!0}},emits:["update-room-code","create-room","join-room","cancel-room"],setup(e,{emit:t}){const n=t;function s(o){n("update-room-code",o.target.value)}function r(){n("join-room")}return(o,a)=>(j(),vt(mn,{title:"远程直连"},{default:Yt(()=>[e.isWaiting?(j(),V("div",Tu,[a[6]||(a[6]=y("p",{class:"waiting-subtitle"},"您的房间号码",-1)),y("div",$u,te(e.generatedCode),1),a[7]||(a[7]=y("div",{class:"spinner"},null,-1)),a[8]||(a[8]=y("p",{class:"waiting-tip"},"等待对方加入...",-1)),y("button",{class:"btn-cancel",type:"button",onClick:a[2]||(a[2]=c=>o.$emit("cancel-room"))},"取消建房")])):(j(),V("div",vu,[y("button",{class:"btn-create",type:"button",onClick:a[0]||(a[0]=c=>o.$emit("create-room"))},[q(pe,{name:"add_circle",size:"22"}),a[3]||(a[3]=nn(" 创建专属传输房间 ",-1))]),a[5]||(a[5]=y("div",{class:"divider"},"或",-1)),y("div",yu,[y("input",{class:"room-code",inputmode:"numeric",maxlength:"4",pattern:"\\d*",placeholder:"输入4位房间号",type:"text",value:e.roomCodeInput,onInput:s,onKeyup:bo(r,["enter"])},null,40,_u),y("button",{class:"btn-primary",type:"button",onClick:a[1]||(a[1]=c=>o.$emit("join-room"))},"加入房间")]),e.pendingDownloads.length?(j(),V("div",bu,[y("div",wu,[a[4]||(a[4]=y("span",null,"待领取文件",-1)),y("span",null,te(e.pendingDownloads.length),1)]),(j(!0),V(fe,null,en(e.pendingDownloads,c=>(j(),V("a",{key:c.transfer_id,class:"pending-download-item",href:c.download_path,target:"_blank",rel:"noopener noreferrer"},[y("div",Iu,[y("strong",{title:c.name},te(c.name),9,Su),y("p",null,te(c.size_label)+" · "+te(c.created_label),1)]),y("span",Cu,[q(pe,{name:"download",size:"18"})])],8,xu))),128))])):Be("",!0)]))]),_:1}))}},Mu={class:"file-info"},Ru=["title"],Au={class:"file-info-right"},Eu=["download","href"],Ou={key:0,class:"progress-bar-container"},Pu={__name:"TransferQueueItem",props:{item:{type:Object,required:!0}},emits:["remove","start-upload","copy"],setup(e){const t=e,n=It(()=>t.item.tone==="success"?{color:"var(--success-green)"}:t.item.tone==="primary"?{color:"var(--accent-blue)"}:t.item.tone==="danger"?{color:"var(--danger-red)"}:{color:"var(--text-secondary)"}),s=It(()=>t.item.kind==="text"?{color:"var(--success-green)",background:"rgba(48, 209, 88, 0.1)"}:{});return(r,o)=>(j(),V("div",{class:St(["batch-item",{"pending-file":e.item.kind==="file"&&e.item.pending}])},[y("div",Mu,[y("div",{class:"file-info-left",style:Le(e.item.kind==="text"?{maxWidth:"70%"}:null)},[y("div",{class:"file-icon-wrapper",style:Le(s.value)},[q(pe,{name:e.item.kind==="text"?"chat_bubble":"draft",size:"18"},null,8,["name"])],4),y("span",{class:"file-name",title:e.item.kind==="text"?e.item.text:e.item.name},te(e.item.kind==="text"?e.item.text:e.item.name),9,Ru)],4),y("div",Au,[y("span",{class:"file-status",style:Le(n.value)},te(e.item.kind==="text"&&e.item.copied?"已复制":e.item.status),5),e.item.kind==="text"?(j(),V("button",{key:0,class:"action-btn",title:"复制文本",type:"button",onClick:o[0]||(o[0]=a=>r.$emit("copy",e.item.id))},[q(pe,{name:e.item.copied?"check":"content_copy",size:"16"},null,8,["name"])])):Be("",!0),e.item.kind==="file"&&e.item.pending?(j(),V("button",{key:1,class:"action-btn primary",title:"发送文件",type:"button",onClick:o[1]||(o[1]=a=>r.$emit("start-upload",e.item.id))},[q(pe,{name:"arrow_upward",size:"16"})])):Be("",!0),e.item.kind==="file"&&e.item.downloadUrl?(j(),V("a",{key:2,class:"action-btn primary",download:e.item.name,href:e.item.downloadUrl,title:"保存文件"},[q(pe,{name:"download",size:"16"})],8,Eu)):Be("",!0),y("button",{class:"action-btn danger",title:"移除记录",type:"button",onClick:o[2]||(o[2]=a=>r.$emit("remove",e.item.id))},[q(pe,{name:"close",size:"16"})])])]),e.item.kind==="file"?(j(),V("div",Ou,[y("div",{class:St(["progress-bar-fill",{success:e.item.tone==="success"}]),style:Le({width:`${e.item.progress}%`})},null,6)])):Be("",!0)],2))}},Du={class:"transfer-panel active"},Nu={class:"card"},Fu={class:"transfer-head"},Uu={class:"connected-to"},Lu={key:0,class:"connection-hint"},Bu={class:"text-input-group"},ju={__name:"TransferPanel",props:{peerName:{type:String,required:!0},connectionType:{type:String,required:!0},networkHint:{type:String,default:""},items:{type:Array,required:!0},hasPendingItems:{type:Boolean,required:!0}},emits:["close","send-text","files-selected","send-all-pending","remove-item","start-upload","copy-item"],setup(e,{emit:t}){const n=e,s=t,r=oe(""),o=oe(!1),a=oe(null),c=oe(null);Ut(()=>n.items.length,async()=>{await Ni(),a.value&&(a.value.scrollTop=a.value.scrollHeight)});function f(){var $;($=c.value)==null||$.click()}function m(){s("send-text",r.value),r.value=""}function p($){const S=Array.from($.target.files||[]);S.length&&s("files-selected",S),$.target.value=""}function _($){var U;o.value=!1;const S=Array.from(((U=$.dataTransfer)==null?void 0:U.files)||[]);S.length&&s("files-selected",S)}return($,S)=>(j(),V("div",Du,[y("div",Nu,[y("div",Fu,[y("div",Uu,[y("h2",null,te(e.peerName),1),y("p",null,te(e.connectionType),1),e.networkHint?(j(),V("small",Lu,te(e.networkHint),1)):Be("",!0)]),y("button",{class:"close-btn",type:"button",onClick:S[0]||(S[0]=U=>$.$emit("close"))},[q(pe,{name:"close",size:"20"})])]),y("div",{class:St(["drop-zone",{"drop-zone-active":o.value}]),onClick:f,onDragenter:S[1]||(S[1]=Pn(U=>o.value=!0,["prevent"])),onDragover:S[2]||(S[2]=Pn(U=>o.value=!0,["prevent"])),onDragleave:S[3]||(S[3]=Pn(U=>o.value=!1,["prevent"])),onDrop:Pn(_,["prevent"])},[q(pe,{class:"drop-zone-icon",name:"cloud_upload",size:"42"}),S[9]||(S[9]=y("p",{class:"drop-zone-text"},"点击或拖拽多个文件到这里",-1)),y("input",{ref_key:"fileInput",ref:c,class:"hidden",multiple:"",type:"file",onChange:p},null,544)],34),y("div",Bu,[Wl(y("input",{"onUpdate:modelValue":S[4]||(S[4]=U=>r.value=U),placeholder:"输入要发送的文本或链接...",type:"text",onKeyup:bo(m,["enter"])},null,544),[[bc,r.value]]),y("button",{title:"发送文本",type:"button",onClick:m},[q(pe,{name:"arrow_upward",size:"20"})])]),y("div",{class:St(["batch-actions",{active:e.hasPendingItems}])},[y("button",{class:"btn-small-primary",type:"button",onClick:S[5]||(S[5]=U=>$.$emit("send-all-pending"))},[q(pe,{name:"send_and_archive",size:"16"}),S[10]||(S[10]=nn(" 一键发送全部 ",-1))])],2),e.items.length?(j(),V("div",{key:0,ref_key:"batchContainer",ref:a,class:"batch-progress-container"},[(j(!0),V(fe,null,en(e.items,U=>(j(),vt(Pu,{key:U.id,item:U,onCopy:S[6]||(S[6]=N=>$.$emit("copy-item",N)),onRemove:S[7]||(S[7]=N=>$.$emit("remove-item",N)),onStartUpload:S[8]||(S[8]=N=>$.$emit("start-upload",N))},null,8,["item"]))),128))],512)):Be("",!0)])]))}};let Vt={deviceId:"",token:""};const Hu="filefast_device_id",Ku="filefast_device_token";function wo(){return!Vt.deviceId||!Vt.token?{}:{"X-Device-ID":Vt.deviceId,"X-Device-Token":Vt.token}}function zu(e={},t=!1){return{...t?{"Content-Type":"application/json"}:{},...wo(),...e}}function Wu(e,t){if(!t||Object.keys(t).length===0)return e;const n=new URLSearchParams;Object.entries(t).forEach(([r,o])=>{o!=null&&o!==""&&n.set(r,String(o))});const s=n.toString();return s?`${e}?${s}`:e}async function Dn(e,t={}){const n=t.body!==void 0,s=await fetch(Wu(e,t.query),{method:t.method||"GET",headers:zu(t.headers,n),body:n?JSON.stringify(t.body):void 0}),r=await s.json().catch(()=>({}));if(!s.ok){const o=new Error(r.error||`Request failed: ${s.status}`);throw o.status=s.status,o}return r.data}const ve={get(e,t={}){return Dn(e,{...t,method:"GET"})},post(e,t,n={}){return Dn(e,{...n,method:"POST",body:t})},put(e,t,n={}){return Dn(e,{...n,method:"PUT",body:t})},patch(e,t,n={}){return Dn(e,{...n,method:"PATCH",body:t})}};function li(e,t){Vt={deviceId:e||"",token:t||""},Gu(Vt)}function Vu(){return wo()}function qu(e){return{Authorization:`Bearer ${e}`}}function Gu(e){typeof document>"u"||(ai(Hu,e.deviceId),ai(Ku,e.token))}function ai(e,t){if(!t){document.cookie=`${e}=; Path=/; Max-Age=0; SameSite=Lax`;return}document.cookie=`${e}=${encodeURIComponent(t)}; Path=/; SameSite=Lax`}function Nn(e){return{headers:qu(e)}}const Kt={login(e,t){return ve.post("/api/admin/login",{username:e,password:t})},stats(e){return ve.get("/api/admin/stats",Nn(e))},config(e){return ve.get("/api/admin/config",Nn(e))},updateConfig(e,t){return ve.put("/api/admin/config",t,Nn(e))},recentTransfers(e){return ve.get("/api/admin/transfers/recent",Nn(e))}},Fn={register(e){return ve.post("/api/devices/register",e)},heartbeat(e){return ve.post("/api/devices/heartbeat",{device_id:e})},listCandidates(e){return ve.get("/api/devices/candidates",{query:{deviceId:e}})},listPendingDownloads(e){return ve.get(`/api/devices/${encodeURIComponent(e)}/pending-downloads`)}},Un={create(e){return ve.post("/api/rooms",{creator_device_id:e})},get(e){return ve.get(`/api/rooms/${encodeURIComponent(e)}`)},join(e,t){return ve.post("/api/rooms/join",{code:e,joiner_device_id:t})},cancel(e,t){return ve.post(`/api/rooms/${encodeURIComponent(e)}/cancel`,{requester_id:t})}},Ju={config(){return ve.get("/api/runtime/config")}},ge={create(e){return ve.post("/api/transfers",e)},presignFallback(e){return ve.post(`/api/transfers/${encodeURIComponent(e)}/fallback/presign`,{})},uploadFallback(e,t,n){return Yu(`/api/transfers/${encodeURIComponent(e)}/fallback/upload`,t,n)},updateStatus(e,t){return ve.patch(`/api/transfers/${encodeURIComponent(e)}/status`,t)}};function Yu(e,t,n){return new Promise((s,r)=>{const o=new XMLHttpRequest;o.open("PUT",e),o.responseType="json",o.setRequestHeader("Content-Type",t.type||"application/octet-stream"),Object.entries(Vu()).forEach(([a,c])=>{o.setRequestHeader(a,c)}),o.upload.onprogress=a=>{!a.lengthComputable||typeof n!="function"||n(Math.round(a.loaded/a.total*100))},o.onload=()=>{const a=o.response||Xu(o.responseText);if(o.status>=200&&o.status<300){s(a.data);return}r(new Error((a==null?void 0:a.error)||`Upload failed: ${o.status}`))},o.onerror=()=>r(new Error("Upload failed")),o.send(t)})}function Xu(e){try{return JSON.parse(e)}catch{return null}}const Zu={class:"container"},Qu={key:0,class:"main-grid"},Ss="filefast-admin-token",Ln="filefast-admin-view",Bn="filefast-device-id",ci="filefast-device-name",Cs="filefast-device-token",ef=15e3,tf=5e3,nf=2e3,sf=3e3,rf=4*1024*1024,of=2e4,lf=16*1024,ui=512*1024,af={__name:"App",setup(e){const t=oe(localStorage.getItem("airshare-theme")||"light"),n=oe(localStorage.getItem(Ln)==="admin"?"admin":"main"),s=oe(!0),r=oe([]),o=oe(""),a=oe(!1),c=oe("----"),f=oe([]),m=oe({name:"--",type:"等待连接",deviceId:"",networkGroupKey:""}),p=oe([]),_=oe("/ws"),$=oe(10240),S=oe(120),U=oe([]),N=oe([]),Y=oe(null),z=oe(localStorage.getItem(Ss)||""),A=oe({id:"",name:"",type:""}),Q=localStorage.getItem(Bn)||"",F=localStorage.getItem(Cs)||"";Q&&F&&li(Q,F);const le=new Map,he=new Map,Te=new Map,$e=new Map;let _t=null,He=null,Je=null,Ke=null,bt=null,de=null,st=null,L=null,B=null,G="",_e="p2p",ze=!1,be=!1,ue=!1,wt=null,Tt=null;const Sn=It(()=>p.value.filter(i=>i.kind==="file"&&i.pending)),$t=It(()=>Sn.value.length>0),Lt=It(()=>!m.value.deviceId||ar(m.value.networkGroupKey)||Uo()?"":"当前是跨网络访问,未配置 TURN 时实时通道可能失败。文本和小文件可回退中转,大文件建议使用 MinIO。");Ut(t,i=>{document.body.setAttribute("data-theme",i),localStorage.setItem("airshare-theme",i)},{immediate:!0}),Ut(n,i=>{if(i==="admin"&&z.value){localStorage.setItem(Ln,"admin");return}localStorage.removeItem(Ln)}),Ut([n,z],([i,l])=>{Ke&&(window.clearInterval(Ke),Ke=null),!(i!=="admin"||!l)&&(Ke=window.setInterval(()=>{ls().catch(u=>{console.error(u)})},5e3))}),Vi(async()=>{_.value=Fo(),await sn(),n.value==="admin"&&z.value&&ls().catch(i=>{console.error(i)}),He=window.setInterval(()=>{w()},ef),_t=window.setInterval(()=>{v()},tf),bt=window.setInterval(()=>{I()},1e4)}),Qs(()=>{_t&&window.clearInterval(_t),He&&window.clearInterval(He),Ke&&window.clearInterval(Ke),bt&&window.clearInterval(bt),b(),ot(),yr(),D()});async function sn(){try{await kt(),await g(),await v()}catch(i){window.alert(`后端连接失败:${i.message}`)}}function Cn(){t.value=t.value==="dark"?"light":"dark"}async function kt(){try{Tn(await Ju.config())}catch(i){console.error(i)}}function Tn(i){i&&(Y.value=i,$.value=Math.round((i.max_minio_fallback_size_bytes||0)/1024/1024),S.value=Math.max(0,Math.round((i.minio_capacity_bytes||0)/1024/1024/1024)))}function d(i){o.value=i.replace(/\D/g,"").slice(0,4)}async function g(){const i=Ao(),l=Eo(i),u=Do(),h=await Fn.register({device_id:i,name:l,type:u,network_group_key:lr()});localStorage.setItem(Bn,h.id),h.auth_token&&(localStorage.setItem(Cs,h.auth_token),li(h.id,h.auth_token)),A.value={id:h.id,name:h.name,type:h.type},await I(),vr()}async function v(){if(A.value.id)try{const i=await ge.create({kind:"text",name:"text-message",content:value,sender_device_id:A.value.id,receiver_device_id:m.value.deviceId});try{await rn(i,value)}catch(l){console.warn("realtime text send failed, fallback to relay",l),await rr(i,value)}p.value.push({id:Ue("text"),transferId:i.id,kind:"text",text:value,status:"已发送",tone:"success",copied:!1})}catch(i){window.alert(`发送文本失败:${i.message}`)}}async function I(){if(!A.value.id){f.value=[];return}try{const i=await Fn.listPendingDownloads(A.value.id);f.value=i.map(l=>({...l,name:rt(l.name),download_path:l.download_path||`/api/transfers/${encodeURIComponent(l.transfer_id)}/fallback/download`,size_label:At(Number(l.size_bytes||0)),created_label:Rn(l.created_at)}))}catch(i){if((i==null?void 0:i.status)===404){f.value=[];return}console.error(i)}}async function w(){if(A.value.id)try{await Fn.heartbeat(A.value.id)}catch(i){console.error(i)}}async function x(){if(!A.value.id){window.alert("当前设备尚未注册到后端");return}try{const i=await Un.create(A.value.id);c.value=i.code,a.value=!0,C(i.code)}catch(i){window.alert(`创建房间失败:${i.message}`)}}async function R(){const i=c.value;b();try{a.value&&i!=="----"&&await Un.cancel(i,A.value.id)}catch(l){console.error(l)}finally{a.value=!1,c.value="----"}}async function k(){if(!(o.value.length<4))try{const i=await Un.join(o.value,A.value.id),l=Rt(i.creator_device_id);o.value="",P({deviceId:i.creator_device_id,name:(l==null?void 0:l.name)||`房间 ${i.code} 创建者`,type:"房间配对成功"})}catch(i){window.alert(`加入房间失败:${i.message}`)}}function C(i){b(),Je=window.setInterval(async()=>{try{const l=await Un.get(i);if(l.status==="joined"&&l.joiner_device_id){const u=Rt(l.joiner_device_id);P({deviceId:l.joiner_device_id,name:(u==null?void 0:u.name)||`房间 ${i} 对端`,type:"房间配对成功"});return}(l.status==="expired"||l.status==="canceled")&&(b(),a.value=!1,c.value="----")}catch(l){console.error(l)}},nf)}function b(){Je&&(window.clearInterval(Je),Je=null)}function P(i){const l=i.deviceId||i.id||"",u=i.connectionType||i.type||"点对点传输";b(),m.value.deviceId!==l&&(ot(),D()),m.value={name:i.name,type:i.connectionType||i.type||"点对点传输",deviceId:i.deviceId||i.id||"",networkGroupKey:i.network_group_key||""},a.value=!1,c.value="----",n.value="transfer",m.value.baseType=u,m.value.type=u,m.value.deviceId=l,We("正在建立实时通道"),it(l,{initiate:!0})}function M(i,l=!1){const u=i.deviceId||i.id||"",h=i.connectionType||i.type||"点对点传输";m.value.deviceId===u&&n.value==="transfer"||(ot(),l||D()),m.value={name:i.name,type:i.connectionType||i.type||"点对点传输",deviceId:u,networkGroupKey:i.network_group_key||""},a.value=!1,c.value="----",n.value="transfer",m.value.baseType=h,m.value.type=h,m.value.deviceId=u,We("正在建立实时通道"),it(u)}function O(){ot(),D(),m.value={name:"--",type:"等待连接",deviceId:"",networkGroupKey:""},n.value="main",m.value.baseType="等待连接",m.value.type="等待连接"}function D(){p.value.forEach(i=>Pe(i)),p.value=[],he.clear()}async function W(i){const l=i.trim();if(l){if(!m.value.deviceId){window.alert("当前没有可用的接收端");return}try{const u=await ge.create({kind:"text",name:"text-message",content:l,sender_device_id:A.value.id,receiver_device_id:m.value.deviceId});ce("transfer.created",m.value.deviceId,{transfer_id:u.id,kind:"text",name:"text-message",content:l,sender_device_id:A.value.id,sender_name:A.value.name,sender_type:A.value.type,receiver_device_id:m.value.deviceId,final_status:"completed",current_channel:"p2p",transport_options:as()}),await ge.updateStatus(u.id,{current_channel:"p2p",final_status:"completed"}),ce("transfer.updated",m.value.deviceId,{transfer_id:u.id,final_status:"completed",current_channel:"p2p"}),p.value.push({id:Ue("text"),transferId:u.id,kind:"text",text:l,status:"已发送",tone:"success",copied:!1})}catch(u){window.alert(`发送文本失败:${u.message}`)}}}function ee(i){const l=i.filter(Boolean).map((u,h)=>({id:Ue(`file-${h}`),kind:"file",file:u,name:rt(u.name),size:At(u.size),sizeBytes:u.size,status:"待发送",tone:"muted",progress:0,pending:!0,transferId:""}));l.length&&p.value.push(...l)}async function J(i){const l=p.value.find(u=>u.id===i);if(!(!l||l.kind!=="file"||!l.pending)){if(!m.value.deviceId){window.alert("当前没有可用的接收端");return}l.pending=!1,l.status="创建传输中...",l.tone="primary";try{const u=await ge.create({kind:"file",name:l.name,size_bytes:l.sizeBytes,sender_device_id:A.value.id,receiver_device_id:m.value.deviceId});if(l.transferId=u.id,l.sizeBytes>rf){await me(l,u);return}ce("transfer.created",m.value.deviceId,{transfer_id:u.id,kind:"file",name:l.name,size_bytes:l.sizeBytes,sender_device_id:A.value.id,sender_name:A.value.name,sender_type:A.value.type,receiver_device_id:m.value.deviceId,final_status:"connecting",current_channel:"p2p",transport_options:as()});try{await ir(l,u)}catch(h){console.warn("realtime file send failed, fallback to relay",h),await or(l,u)}}catch(u){l.pending=!0,l.status=`发送失败:${u.message}`,l.tone="danger"}}}async function me(i,l){i.progress=0,i.status="上传准备中...";try{if(!l.fallback_allowed)throw new Error("当前文件过大,且未启用 MinIO 回退");await ge.presignFallback(i.transferId),ce("transfer.updated",m.value.deviceId,{transfer_id:i.transferId,final_status:"fallback_uploading",current_channel:"minio"}),i.status="上传中...";const u=await ge.uploadFallback(i.transferId,i.file,h=>{i.progress=Math.max(1,Math.min(h,99))});await ge.updateStatus(i.transferId,{current_channel:"minio",final_status:"completed"}),ce("transfer.updated",m.value.deviceId,{transfer_id:i.transferId,final_status:"completed",current_channel:"minio"}),ce("transfer.file",m.value.deviceId,{transfer_id:i.transferId,name:i.name,download_url:u.download_path||u.download_url}),i.progress=100,i.status="上传完成",i.tone="success"}catch(u){i.pending=!0,i.status=`上传失败:${u.message}`,i.tone="danger"}}async function we(){for(const i of Sn.value)await J(i.id)}async function Ne(i){const l=p.value.find(u=>u.id===i);if(l&&Pe(l),p.value=p.value.filter(u=>u.id!==i),!(!(l!=null&&l.transferId)||l.tone==="success"))try{await ge.updateStatus(l.transferId,{final_status:"cancelled"}),ce("transfer.updated",m.value.deviceId,{transfer_id:l.transferId,final_status:"cancelled"})}catch(u){console.error(u)}}async function Fe(i){const l=p.value.find(u=>u.id===i);if(!(!l||l.kind!=="text"))try{await Mt(l.text),l.copied=!0,window.setTimeout(()=>{const u=p.value.find(h=>h.id===i);u&&u.kind==="text"&&(u.copied=!1)},2e3)}catch{window.alert("复制失败")}}async function Mt(i){var u;if((u=navigator.clipboard)!=null&&u.writeText&&window.isSecureContext){await navigator.clipboard.writeText(i);return}const l=document.createElement("textarea");l.value=i,l.setAttribute("readonly","readonly"),l.style.position="fixed",l.style.top="0",l.style.left="-9999px",l.style.opacity="0",document.body.appendChild(l),l.focus(),l.select(),l.setSelectionRange(0,l.value.length);try{if(!document.execCommand("copy"))throw new Error("copy command failed")}finally{document.body.removeChild(l)}}function $n(i){const l=le.get(i);l&&(window.clearInterval(l),le.delete(i))}function ke(i){return new Promise((l,u)=>{const h=new FileReader;h.onload=()=>l(String(h.result||"")),h.onerror=()=>u(new Error("Failed to read file")),h.readAsDataURL(i)})}function Pe(i){if($n(i.id),i.ownedDownloadUrl&&i.downloadUrl)try{URL.revokeObjectURL(i.downloadUrl)}catch(l){console.error(l)}i.transferId&&he.delete(i.transferId)}function Bt(i,l,u=!1){if(i.ownedDownloadUrl&&i.downloadUrl&&i.downloadUrl!==l)try{URL.revokeObjectURL(i.downloadUrl)}catch(h){console.error(h)}i.downloadUrl=l,i.ownedDownloadUrl=u}async function rn(i,l){const u=await dr(m.value.deviceId);kn(u,{type:"text",transfer_id:i.id,text:l,sender_device_id:A.value.id,sender_name:A.value.name,sender_type:A.value.type});const h=gr();await ge.updateStatus(i.id,{current_channel:h,final_status:"completed"})}async function rr(i,l){ce("transfer.created",m.value.deviceId,{transfer_id:i.id,kind:"text",name:"text-message",content:l,sender_device_id:A.value.id,sender_name:A.value.name,sender_type:A.value.type,receiver_device_id:m.value.deviceId,final_status:"completed",current_channel:"p2p"}),await ge.updateStatus(i.id,{current_channel:"p2p",final_status:"completed"})}async function ir(i,l){var E;const u=await dr(m.value.deviceId);i.status="正在通过 WebRTC 发送...",i.progress=1,kn(u,{type:"file-meta",transfer_id:l.id,name:i.name,mime_type:((E=i.file)==null?void 0:E.type)||"application/octet-stream",size_bytes:i.sizeBytes,sender_device_id:A.value.id,sender_name:A.value.name,sender_type:A.value.type});let h=0;for(;hui;)await To(20)}function To(i){return new Promise(l=>{window.setTimeout(l,i)})}function $o(i,l,u){return new Promise((h,T)=>{const E=window.setTimeout(()=>{T(new Error(u))},l);i.then(se=>{window.clearTimeout(E),h(se)}).catch(se=>{window.clearTimeout(E),T(se)})})}async function ko(){const i=window.prompt("管理员用户名","admin");if(i===null)return;const l=window.prompt("管理员密码");if(l!==null)try{const u=await Kt.login(i.trim()||"admin",l);z.value=u.token,localStorage.setItem(Ss,u.token),await ls(),n.value="admin"}catch(u){window.alert(`管理员登录失败:${u.message}`)}}function Mo(){n.value="main"}async function ls(){if(z.value)try{const[i,l,u]=await Promise.all([Kt.stats(z.value),Kt.config(z.value),Kt.recentTransfers(z.value)]);Tn(l),U.value=xr(i.stats||{},i.minio||{}),N.value=u.map(h=>Ir(h))}catch(i){throw(i==null?void 0:i.status)===401&&(localStorage.removeItem(Ss),localStorage.removeItem(Ln),z.value="",n.value="main"),i}}async function Ro(){if(!z.value||!Y.value){window.alert("当前没有可用的管理员会话");return}try{const i={...Y.value,max_minio_fallback_size_bytes:Math.max(0,$.value)*1024*1024,minio_capacity_bytes:Math.max(0,S.value)*1024*1024*1024},l=await Kt.updateConfig(z.value,i);Tn(l);{const u=await Kt.stats(z.value);U.value=xr(u.stats||{},u.minio||{})}window.alert("配置已保存")}catch(i){window.alert(`保存配置失败:${i.message}`)}}function Ao(){let i=localStorage.getItem(Bn);return i||(i=typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():`web-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,localStorage.setItem(Bn,i)),i}function Eo(i){let l=localStorage.getItem(ci);return(!l||Oo(l,i))&&(l=`${Po()} ${i.slice(0,4)}`,localStorage.setItem(ci,l)),l}function Oo(i,l){const u=String(i||"").trim(),h=l.slice(0,4);return!u||!h||!u.endsWith(` ${h}`)?!1:/^(android|iphone|ipad|linux|macintel|macos|windows|win32|web)\s/i.test(u)}function Po(){const i=`${navigator.userAgent} ${navigator.platform}`.toLowerCase();return i.includes("iphone")?"iPhone":i.includes("ipad")?"iPad":i.includes("android")?"Android":i.includes("windows")||i.includes("win32")?"Windows":i.includes("mac os")||i.includes("macintosh")||i.includes("macintel")?"macOS":i.includes("linux")?"Linux":"Web"}function Do(){const i=`${navigator.userAgent} ${navigator.platform}`.toLowerCase();return i.includes("iphone")||i.includes("android")||i.includes("mobile")?"phone":i.includes("ipad")||i.includes("tablet")?"tablet":"desktop"}function rt(i,l="file"){const u=String(i||"").trim();if(!u)return l;if(!/%[0-9A-Fa-f]{2}/.test(u))return u;try{return decodeURIComponent(u)}catch{return u}}function No(i){return i==="phone"?"smartphone":i==="tablet"?"tablet_mac":"laptop_mac"}function jt(i){return i==="phone"?"手机":i==="tablet"?"平板":"桌面端"}function Fo(){return`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/ws`}function Uo(){var i;return Array.isArray((i=Y.value)==null?void 0:i.turn_urls)&&Y.value.turn_urls.some(l=>String(l||"").trim())}function lr(){const i=String(window.location.hostname||"").trim().toLowerCase();return i?i==="localhost"||i==="127.0.0.1"||i==="::1"||i.endsWith(".local")||Lo(i)?i:"":"local"}function ar(i){const l=lr();return!!l&&i===l}function Lo(i){const l=i.split(".");if(l.length!==4||l.some(T=>!/^\d+$/.test(T)))return!1;const[u,h]=l.map(T=>Number(T));return u===10||u===127||u===192&&h===168?!0:u===172&&h>=16&&h<=31}function cr(i){const l=Array.isArray(i==null?void 0:i.turn_urls)?i.turn_urls.map(u=>String(u||"").trim()).filter(Boolean):[];return l.length?[{urls:l,username:(i==null?void 0:i.turn_username)||"",credential:(i==null?void 0:i.turn_password)||""}]:[]}function as(){var i,l;return{ice_servers:cr(Y.value),p2p_connect_timeout_sec:((i=Y.value)==null?void 0:i.p2p_connect_timeout_sec)||15,turn_connect_timeout_sec:((l=Y.value)==null?void 0:l.turn_connect_timeout_sec)||20}}function ur(){return typeof RTCPeerConnection<"u"}function cs(){wt=null,Tt=null}function Bo(){return wt||(wt=new Promise(i=>{Tt=i})),wt}function fr(i){Tt&&Tt(i),wt=Promise.resolve(i),Tt=null}function We(i=""){if(!m.value.deviceId)return;const l=m.value.baseType||m.value.type||"点对点传输";m.value={...m.value,type:i?`${l} · ${i}`:l}}function jo(i){return!L||G!==i||L.signalingState==="closed"||["failed","disconnected","closed"].includes(L.connectionState)||["failed","disconnected","closed"].includes(L.iceConnectionState)?!0:!B||B.readyState==="closed"}async function it(i,l={}){return!i||!ur()?null:(jo(i)&&(ot(),Ho(i)),l.initiate&&L.signalingState==="stable"&&await Ko(i),L)}function Ho(i){G=i,_e="p2p",ze=!1,be=!1,ue=!1,$e.delete(i),cs(),L=new RTCPeerConnection({iceServers:cr(Y.value)}),B=L.createDataChannel("filefast-control",{negotiated:!0,id:0,ordered:!0}),zo(B),L.onicecandidate=({candidate:l})=>{if(l)try{ce("webrtc.candidate",i,{candidate:l})}catch(u){console.error(u)}},L.onconnectionstatechange=()=>{if(L){if(us(),L.connectionState==="connected"){We(_e==="turn"?"TURN 中继已连接":"WebRTC 直连已连接");return}if(L.connectionState==="connecting"){We("实时通道连接中");return}if(L.connectionState==="failed"){We("实时通道连接失败");return}(L.connectionState==="disconnected"||L.connectionState==="closed")&&We("实时通道已断开")}},L.oniceconnectionstatechange=()=>{us()}}async function Ko(i){if(L)try{ze=!0,await L.setLocalDescription(),ce("webrtc.description",i,{description:L.localDescription})}finally{ze=!1}}function zo(i){B=i,i.bufferedAmountLowThreshold=ui/2,i.onopen=()=>{fr(i),We(_e==="turn"?"TURN 中继已连接":"WebRTC 直连已连接"),us()},i.onclose=()=>{B===i&&(B=null,cs(),We("实时通道已关闭"))},i.onerror=l=>{console.error(l)},i.onmessage=l=>{Wo(l.data)},i.readyState==="open"&&fr(i)}function ot(){he.clear(),G&&$e.delete(G),B&&(B.onopen=null,B.onclose=null,B.onerror=null,B.onmessage=null,B.close(),B=null),L&&(L.onicecandidate=null,L.onconnectionstatechange=null,L.oniceconnectionstatechange=null,L.close(),L=null),G="",_e="p2p",ze=!1,be=!1,ue=!1,cs()}async function dr(i){if(!ur())throw new Error("当前浏览器不支持 WebRTC");if(await it(i,{initiate:!0}),(B==null?void 0:B.readyState)==="open")return B;const l=await $o(Bo(),of,"WebRTC 连接超时");if(!l||l.readyState!=="open")throw new Error("实时通道未建立");return l}function kn(i,l){if(!i||i.readyState!=="open")throw new Error("实时通道未就绪");i.send(JSON.stringify(l))}function Wo(i){try{const l=JSON.parse(String(i||"{}"));if(l.type==="text"){Vo(l);return}if(l.type==="file-meta"){qo(l);return}if(l.type==="file-chunk"){Go(l);return}l.type==="file-complete"&&Jo(l)}catch(l){console.error(l)}}function Vo(i){var T;const l=i.sender_device_id||G,u={id:l,name:i.sender_name||((T=Rt(l))==null?void 0:T.name)||`设备 ${lt(l)}`,type:jt(i.sender_type||"desktop"),connectionType:_e==="turn"?"TURN 中继已连接":"WebRTC 直连已连接"};M(u,!0);const h=p.value.find(E=>E.transferId===i.transfer_id);if(h&&h.kind==="text"){h.text=i.text||"",h.status="已接收",h.tone="success";return}p.value.push({id:Ue("incoming-text"),transferId:i.transfer_id,kind:"text",text:i.text||"",status:"已接收",tone:"success",copied:!1})}function qo(i){var T;const l=i.sender_device_id||G,u={id:l,name:i.sender_name||((T=Rt(l))==null?void 0:T.name)||`设备 ${lt(l)}`,type:jt(i.sender_type||"desktop"),connectionType:_e==="turn"?"TURN 中继已连接":"WebRTC 直连已连接"};M(u,!0),he.set(i.transfer_id,{name:rt(i.name,"file"),mimeType:i.mime_type||"application/octet-stream",sizeBytes:Number(i.size_bytes||0),receivedBytes:0,chunks:[]});let h=p.value.find(E=>E.transferId===i.transfer_id);h?(h.status="正在接收...",h.tone="primary",h.progress=0):(h={id:Ue("incoming-file"),transferId:i.transfer_id,kind:"file",name:rt(i.name,"file"),size:At(Number(i.size_bytes||0)),sizeBytes:Number(i.size_bytes||0),status:"正在接收...",tone:"primary",progress:0,pending:!1,downloadUrl:"",ownedDownloadUrl:!1},p.value.push(h))}function Go(i){const l=he.get(i.transfer_id);if(!l)return;const u=So(String(i.chunk_base64||""));l.receivedBytes+=Number(i.chunk_size||u.byteLength||0),l.chunks.push(u);const h=p.value.find(T=>T.transferId===i.transfer_id);if(h){const T=l.sizeBytes>0?l.receivedBytes/l.sizeBytes*100:0;h.progress=Math.max(1,Math.min(99,Math.round(T))),h.status="正在接收...",h.tone="primary"}}function Jo(i){const l=he.get(i.transfer_id);if(!l)return;const u=p.value.find(E=>E.transferId===i.transfer_id);if(!u){he.delete(i.transfer_id);return}const h=new Blob(l.chunks,{type:l.mimeType||"application/octet-stream"}),T=URL.createObjectURL(h);Bt(u,T,!0),u.progress=100,u.status="可保存",u.tone="success",he.delete(i.transfer_id)}function pr(i){return A.value.id.localeCompare(i)>0}function Mn(i,l="等待实时数据"){const u=Rt(i);return{id:i,deviceId:i,name:(u==null?void 0:u.name)||`设备 ${lt(i)}`,type:jt((u==null?void 0:u.type)||"desktop"),connectionType:l,network_group_key:(u==null?void 0:u.network_group_key)||""}}async function hr(i){const u=(i.payload||{}).description,h=i.device_id||"";if(!u||!h)return;M(Mn(h),!0);const T=await it(h);if(!T)return;const E=pr(h),se=!ze&&(T.signalingState==="stable"||ue),Et=u.type==="offer"&&!se;be=!E&&Et,!be&&(ue=u.type==="answer",await T.setRemoteDescription(u),ue=!1,u.type==="offer"&&(await T.setLocalDescription(),ce("webrtc.description",h,{description:T.localDescription})))}async function mr(i){const l=i.payload||{},u=i.device_id||"";if(!l.candidate||!u)return;(n.value!=="transfer"||m.value.deviceId!==u)&&M(Mn(u),!0);const h=await it(u);if(h)try{await h.addIceCandidate(l.candidate)}catch(T){be||console.error(T)}}async function us(){if(!(!L||L.connectionState!=="connected"))try{const i=await L.getStats();let l=null;if(i.forEach(E=>{E.type==="transport"&&E.selectedCandidatePairId&&(l=i.get(E.selectedCandidatePairId)||l)}),l||i.forEach(E=>{E.type==="candidate-pair"&&E.state==="succeeded"&&(E.nominated||E.selected)&&(l=E)}),!l)return;const u=i.get(l.localCandidateId),h=i.get(l.remoteCandidateId),T=(u==null?void 0:u.candidateType)==="relay"||(h==null?void 0:h.candidateType)==="relay";_e=T?"turn":"p2p",(B==null?void 0:B.readyState)==="open"&&We(T?"TURN 中继已连接":"WebRTC 直连已连接")}catch(i){console.error(i)}}function gr(){return _e==="turn"?"turn":"p2p"}function vr(){if(!A.value.id)return;const i=localStorage.getItem(Cs)||"";i&&(yr(),de=new WebSocket(`${_.value}?deviceId=${encodeURIComponent(A.value.id)}&deviceToken=${encodeURIComponent(i)}`),de.addEventListener("message",l=>{Xo(l.data)}),de.addEventListener("close",()=>{de=null,Yo()}),de.addEventListener("error",()=>{de==null||de.close()}))}function yr(){if(st&&(window.clearTimeout(st),st=null),!de)return;const i=de;de=null,i.onclose=null,i.close()}function Yo(){st||!A.value.id||(st=window.setTimeout(()=>{st=null,vr()},sf))}function ce(i,l,u){!de||de.readyState!==WebSocket.OPEN||!l||de.send(JSON.stringify({type:i,target_device_id:l,payload:u}))}function Xo(i){try{const l=JSON.parse(i);if(l.type==="presence.update"){v();return}if(l.type==="webrtc.description"){hr(l);return}if(l.type==="webrtc.candidate"){mr(l);return}if(l.type==="transfer.created"){_r(l);return}if(l.type==="transfer.updated"){br(l);return}if(l.type==="transfer.file"){wr(l);return}l.type==="peer.session.closed"&&Zo(l)}catch(l){console.error(l)}}function Zo(i){const l=i.device_id||"";!l||m.value.deviceId!==l||(ot(),D(),m.value={name:"--",type:"绛夊緟杩炴帴",baseType:"绛夊緟杩炴帴",deviceId:"",networkGroupKey:""},n.value="main")}function _r(i){var E;const l=i.payload||{},u=i.device_id||l.sender_device_id||"",h={id:u,name:l.sender_name||((E=Rt(u))==null?void 0:E.name)||`Device ${lt(u)}`,type:jt(l.sender_type||"desktop")};if(h.connectionType="等待实时数据",M(h,!0),!p.value.find(se=>se.transferId===l.transfer_id)){if(l.kind==="text"){p.value.push({id:Ue("incoming-text"),transferId:l.transfer_id,kind:"text",text:l.content||"",status:"已接收",tone:"success",copied:!1});return}p.value.push({id:Ue("incoming-file"),transferId:l.transfer_id,kind:"file",name:rt(l.name,"file"),size:At(Number(l.size_bytes||0)),sizeBytes:Number(l.size_bytes||0),status:"接收中...",tone:"primary",progress:35,pending:!1,downloadUrl:"",ownedDownloadUrl:!1})}}function br(i){const l=i.payload||{},u=p.value.find(h=>h.transferId===l.transfer_id);if(u&&u.kind==="file"){if(l.final_status==="completed"){u.progress=100,u.status="已接收",u.tone="success",u.downloadUrl&&(u.status="可保存");return}l.final_status==="cancelled"&&(u.status="已取消",u.tone="danger")}}function wr(i){const l=i.payload||{};let u=p.value.find(h=>h.transferId===l.transfer_id);!u&&l.transfer_id&&(u={id:Ue("incoming-file"),transferId:l.transfer_id,kind:"file",name:rt(l.name,"file"),size:"",sizeBytes:0,status:"可保存",tone:"success",progress:100,pending:!1,downloadUrl:"",ownedDownloadUrl:!1},p.value.push(u)),!(!u||u.kind!=="file")&&(Bt(u,l.download_url||l.data_url||"",!1),u.status="可保存",u.progress=100,u.tone="success")}function Rt(i){return r.value.find(l=>l.id===i)}function xr(i,l={}){return[{label:"在线设备",value:`${i.devices_online||0}`,tone:"blue"},{label:"待加入房间",value:`${i.rooms_waiting||0}`,tone:"cyan"},{label:"有效传输",value:`${i.transfers_total||0}`,tone:"default"},{label:"累计传输",value:`${i.transfers_cumulative||0}`,tone:"default"},{kind:"minio",label:"MinIO 剩余容量",value:fs(l.remaining_bytes||0),tone:Number(l.usage_percent||0)>=85?"danger":Number(l.usage_percent||0)>=60?"cyan":"blue",percent:Math.max(0,100-Number(l.usage_percent||0)),detail:`已用 ${fs(l.used_bytes||0)} / 总计 ${fs(l.capacity_bytes||0)}`,kicker:`存档 ${l.object_count||0} 份`}]}function fs(i){const l=Number(i||0);if(!l||l<=0)return"0 GB";const u=["B","KB","MB","GB","TB"],h=Math.min(Math.floor(Math.log(l)/Math.log(1024)),u.length-1),T=l/1024**h,E=h>=3?2:T>=10?1:2;return`${T.toFixed(E)} ${u[h]}`}function Ir(i){const l=i.final_status==="completed",u=i.final_status==="failed"||i.final_status==="cancelled";return{time:Rn(i.created_at),peer:`${lt(i.sender_device_id)} -> ${lt(i.receiver_device_id)}`,type:i.kind==="text"?"文本消息":`文件 ${i.name}`,size:At(Number(i.size_bytes||0)),status:l?`已完成 (${i.current_channel||"p2p"})`:u?`已结束 (${i.final_status})`:`进行中 (${i.final_status||"pending"})`,tone:l?"success":u?"danger":"primary"}}function lt(i){return i?i.slice(0,8):"--"}function Rn(i){if(!i)return"刚刚";const l=new Date(i),u=Date.now()-l.getTime();if(!Number.isFinite(u))return"刚刚";const h=Math.max(0,Math.floor(u/1e3));if(h<60)return`${h} 秒前`;const T=Math.floor(h/60);if(T<60)return`${T} 分钟前`;const E=Math.floor(T/60);return E<24?`${E} 小时前`:`${Math.floor(E/24)} 天前`}function Ue(i){return`${i}-${Date.now()}-${Math.random().toString(36).slice(2,8)}`}function At(i){if(!i||i<=0)return"0 B";const l=["B","KB","MB","GB","TB"],u=Math.min(Math.floor(Math.log(i)/Math.log(1024)),l.length-1),h=i/1024**u,T=h>=10||u===0?0:1;return`${h.toFixed(T)} ${l[u]}`}v=async function(){return A.value.id?Fn.listCandidates(A.value.id).then(l=>{const u=Array.isArray(l)?l:[];r.value=u.map(h=>({...h,description:`${jt(h.type)} · 最近活跃 ${Rn(h.last_seen_at)}`,icon:No(h.type),connectionType:ar(h.network_group_key)?"局域网直连优先":"跨网络实时传输"})),s.value=r.value.length===0}).catch(l=>{s.value=!1,console.error(l)}):Promise.resolve()},P=function(l){const u=l.deviceId||l.id||"",h=l.connectionType||l.type||"点对点传输";b(),m.value.deviceId!==u&&(ot(),D()),m.value={name:l.name,type:h,baseType:h,deviceId:u,networkGroupKey:l.network_group_key||""},a.value=!1,c.value="----",n.value="transfer",We("正在建立实时通道"),it(u,{initiate:!0})},M=function(l,u=!1){const h=l.deviceId||l.id||"",T=l.connectionType||l.type||"点对点传输";m.value.deviceId===h&&n.value==="transfer"||(ot(),u||D()),m.value={name:l.name,type:T,baseType:T,deviceId:h,networkGroupKey:l.network_group_key||""},a.value=!1,c.value="----",n.value="transfer",h&&(We("正在建立实时通道"),it(h))},O=function(){m.value.deviceId&&ce("peer.session.closed",m.value.deviceId,{}),ot(),D(),m.value={name:"--",type:"等待连接",baseType:"等待连接",deviceId:"",networkGroupKey:""},n.value="main"},W=async function(l){const u=l.trim();if(u){if(!m.value.deviceId){window.alert("当前没有可用的接收端");return}try{const h=await ge.create({kind:"text",name:"text-message",content:u,sender_device_id:A.value.id,receiver_device_id:m.value.deviceId});try{await rn(h,u)}catch(T){console.warn("realtime text send failed, fallback to relay",T),await rr(h,u)}p.value.push({id:Ue("text"),transferId:h.id,kind:"text",text:u,status:"已发送",tone:"success",copied:!1})}catch(h){window.alert(`发送文本失败:${h.message}`)}}};function Sr(i,l){ce("transfer.file",m.value.deviceId,{transfer_id:i.transferId,name:i.name,download_url:l.download_path||l.download_url})}function Cr(i,l,{onProgress:u}={}){if(!(i!=null&&i.file))return Promise.reject(new Error("未找到待上传文件"));if(!(l!=null&&l.fallback_allowed))return Promise.reject(new Error("MinIO 存档未启用"));const h=l.id;if(Te.has(h))return Te.get(h);const T=(async()=>(await ge.presignFallback(h),ge.uploadFallback(h,i.file,E=>{typeof u=="function"&&u(E)})))().finally(()=>{Te.delete(h)});return Te.set(h,T),T}async function Tr(i,l,u){await ge.updateStatus(l.id,{current_channel:"minio",final_status:"completed"}),ce("transfer.updated",m.value.deviceId,{transfer_id:l.id,final_status:"completed",current_channel:"minio"}),Sr(i,u),i.progress=100,i.status="已上传到 MinIO,对方可直接领取",i.tone="success"}async function Qo(i,l){const u=$e.get(i);if(!(!(u!=null&&u.length)||!(l!=null&&l.remoteDescription))){$e.delete(i);for(const h of u)try{await l.addIceCandidate(h)}catch(T){console.error(T)}}}return me=async function(l,u){l.progress=Math.max(5,l.progress||0),l.status="正在切换到 MinIO...",l.tone="primary";try{ce("transfer.updated",m.value.deviceId,{transfer_id:l.transferId,final_status:"fallback_uploading",current_channel:"minio"});const h=await Cr(l,u,{onProgress:T=>{l.progress=Math.max(5,Math.min(T,99))}});await Tr(l,u,h)}catch(h){l.pending=!0,l.status=`上传失败:${h.message}`,l.tone="danger"}},hr=async function(l){const h=(l.payload||{}).description,T=l.device_id||"";if(!h||!T)return;M(Mn(T),!0);const E=await it(T);if(!E)return;const se=pr(T),Et=!ze&&(E.signalingState==="stable"||ue),el=h.type==="offer"&&!Et;if(be=!se&&el,!be&&!(h.type==="answer"&&(E.signalingState!=="have-local-offer"||!E.localDescription))){try{ue=h.type==="answer",await E.setRemoteDescription(h),await Qo(T,E)}catch(ds){console.error(ds)}finally{ue=!1}if(h.type==="offer")try{await E.setLocalDescription(),ce("webrtc.description",T,{description:E.localDescription})}catch(ds){console.error(ds)}}},mr=async function(l){const u=l.payload||{},h=l.device_id||"",T=u.candidate;if(!T||!h)return;(n.value!=="transfer"||m.value.deviceId!==h)&&M(Mn(h),!0);const E=await it(h);if(E){if(!E.remoteDescription){const se=$e.get(h)||[];se.push(T),$e.set(h,se);return}try{await E.addIceCandidate(T)}catch(se){be||console.error(se)}}},J=async function(l){const u=p.value.find(h=>h.id===l);if(!(!u||u.kind!=="file"||!u.pending)){if(!m.value.deviceId){window.alert("当前没有可用的接收端");return}u.pending=!1,u.status="创建传输中...",u.tone="primary";try{const h=await ge.create({kind:"file",name:u.name,size_bytes:u.sizeBytes,sender_device_id:A.value.id,receiver_device_id:m.value.deviceId});u.transferId=h.id;const T=h.fallback_allowed?Cr(u,h).catch(E=>{throw console.warn("minio backup sync failed",E),E}):Promise.resolve(null);ce("transfer.created",m.value.deviceId,{transfer_id:h.id,kind:"file",name:u.name,size_bytes:u.sizeBytes,sender_device_id:A.value.id,sender_name:A.value.name,sender_type:A.value.type,receiver_device_id:m.value.deviceId,final_status:"connecting",current_channel:"p2p",transport_options:as()});try{if(await ir(u,h),h.fallback_allowed){u.status="实时传输完成,正在同步云端备份...",u.tone="primary";try{const E=await T;E&&(Sr(u,E),u.status="已发送,2 小时内可离线领取")}catch(E){u.status=`实时传输成功,但 MinIO 备份失败:${E.message}`,u.tone="danger";return}u.tone="success"}}catch(E){console.warn("realtime file send failed, fallback to minio",E);try{const se=await T;if(se){await Tr(u,h,se);return}}catch(se){console.warn("minio backup sync failed after realtime failure",se)}await or(u,h)}}catch(h){u.pending=!0,u.status=`发送失败:${h.message}`,u.tone="danger"}}},_r=function(l){var se;const u=l.payload||{},h=l.device_id||u.sender_device_id||"",T={id:h,name:u.sender_name||((se=Rt(h))==null?void 0:se.name)||`设备 ${lt(h)}`,type:jt(u.sender_type||"desktop"),connectionType:"等待实时数据"};if(M(T,!0),!p.value.find(Et=>Et.transferId===u.transfer_id)){if(u.kind==="text"){u.content&&p.value.push({id:Ue("incoming-text"),transferId:u.transfer_id,kind:"text",text:u.content||"",status:"已接收",tone:"success",copied:!1});return}p.value.push({id:Ue("incoming-file"),transferId:u.transfer_id,kind:"file",name:rt(u.name,"file"),size:At(Number(u.size_bytes||0)),sizeBytes:Number(u.size_bytes||0),status:"等待接收...",tone:"primary",progress:5,pending:!1,downloadUrl:"",ownedDownloadUrl:!1})}},br=function(l){const u=l.payload||{},h=p.value.find(T=>T.transferId===u.transfer_id);if(h&&h.kind==="file"){if(u.final_status==="completed"){h.progress=100,h.status=h.downloadUrl?"可保存":"传输完成",h.tone="success";return}if(u.final_status==="cancelled"){h.status="已取消",h.tone="danger";return}u.final_status==="fallback_uploading"&&(h.status="发送端正在上传回退文件...",h.tone="primary")}},wr=function(l){const u=l.payload||{};let h=p.value.find(T=>T.transferId===u.transfer_id);!h&&u.transfer_id&&(h={id:Ue("incoming-file"),transferId:u.transfer_id,kind:"file",name:rt(u.name,"file"),size:"",sizeBytes:0,status:"可保存",tone:"success",progress:100,pending:!1,downloadUrl:"",ownedDownloadUrl:!1},p.value.push(h)),!(!h||h.kind!=="file")&&(Bt(h,u.download_url||u.data_url||"",!1),h.status="可保存",h.progress=100,h.tone="success")},Ir=function(l){const u=l.final_status==="completed",h=l.final_status==="failed"||l.final_status==="cancelled",T=rt(l.name,"file");return{time:Rn(l.created_at),peer:`${lt(l.sender_device_id)} -> ${lt(l.receiver_device_id)}`,type:l.kind==="text"?"文本消息":`文件 ${T}`,size:At(Number(l.size_bytes||0)),status:u?`已完成(${l.current_channel||"p2p"})`:h?`已结束(${l.final_status})`:`进行中(${l.final_status||"pending"})`,tone:u?"success":h?"danger":"primary"}},(i,l)=>(j(),V("div",null,[y("div",Zu,[q(Oc,{theme:t.value,onToggleTheme:Cn},null,8,["theme"]),n.value==="main"?(j(),V("div",Qu,[q(gu,{devices:r.value,"is-scanning":s.value,"local-device-name":A.value.name,onSelectDevice:P},null,8,["devices","is-scanning","local-device-name"]),q(ku,{"generated-code":c.value,"is-waiting":a.value,"pending-downloads":f.value,"room-code-input":o.value,onCancelRoom:R,onCreateRoom:x,onJoinRoom:k,onUpdateRoomCode:d},null,8,["generated-code","is-waiting","pending-downloads","room-code-input"])])):Be("",!0),n.value==="transfer"?(j(),vt(ju,{key:1,"connection-type":m.value.type,"has-pending-items":$t.value,items:p.value,"network-hint":Lt.value,"peer-name":m.value.name,onClose:O,onCopyItem:Fe,onFilesSelected:ee,onRemoveItem:Ne,onSendAllPending:we,onSendText:W,onStartUpload:J},null,8,["connection-type","has-pending-items","items","network-hint","peer-name"])):Be("",!0),n.value==="admin"?(j(),vt(ou,{key:2,"file-limit":$.value,"minio-capacity":S.value,records:N.value,stats:U.value,onExit:Mo,onSaveConfig:Ro,"onUpdate:fileLimit":l[0]||(l[0]=u=>$.value=u),"onUpdate:minioCapacity":l[1]||(l[1]=u=>S.value=u)},null,8,["file-limit","minio-capacity","records","stats"])):Be("",!0)]),q(Rc,{onRequestAdmin:ko})]))}};Tc(af).mount("#app");
+**/let Ns;const Vr=typeof window<"u"&&window.trustedTypes;if(Vr)try{Ns=Vr.createPolicy("vue",{createHTML:e=>e})}catch{}const _o=Ns?e=>Ns.createHTML(e):e=>e,Qa="http://www.w3.org/2000/svg",ec="http://www.w3.org/1998/Math/MathML",ct=typeof document<"u"?document:null,qr=ct&&ct.createElement("template"),tc={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?ct.createElementNS(Qa,e):t==="mathml"?ct.createElementNS(ec,e):n?ct.createElement(e,{is:n}):ct.createElement(e);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>ct.createTextNode(e),createComment:e=>ct.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>ct.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,o){const a=n?n.previousSibling:t.lastChild;if(r&&(r===o||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===o||!(r=r.nextSibling)););else{qr.innerHTML=_o(s==="svg"?``:s==="mathml"?``:e);const c=qr.content;if(s==="svg"||s==="mathml"){const f=c.firstChild;for(;f.firstChild;)c.appendChild(f.firstChild);c.removeChild(f)}t.insertBefore(c,n)}return[a?a.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},nc=Symbol("_vtc");function sc(e,t,n){const s=e[nc];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const Gr=Symbol("_vod"),rc=Symbol("_vsh"),ic=Symbol(""),oc=/(?:^|;)\s*display\s*:/;function lc(e,t,n){const s=e.style,r=ae(n);let o=!1;if(n&&!r){if(t)if(ae(t))for(const a of t.split(";")){const c=a.slice(0,a.indexOf(":")).trim();n[c]==null&&zn(s,c,"")}else for(const a in t)n[a]==null&&zn(s,a,"");for(const a in n)a==="display"&&(o=!0),zn(s,a,n[a])}else if(r){if(t!==n){const a=s[ic];a&&(n+=";"+a),s.cssText=n,o=oc.test(n)}}else t&&e.removeAttribute("style");Gr in e&&(e[Gr]=o?s.display:"",e[rc]&&(s.display="none"))}const Jr=/\s*!important$/;function zn(e,t,n){if(H(n))n.forEach(s=>zn(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=ac(e,t);Jr.test(n)?e.setProperty(Ct(s),n.replace(Jr,""),"important"):e[s]=n}}const Yr=["Webkit","Moz","ms"],ws={};function ac(e,t){const n=ws[t];if(n)return n;let s=Ae(t);if(s!=="filter"&&s in e)return ws[t]=s;s=Qn(s);for(let r=0;rxs||(dc.then(()=>xs=0),xs=Date.now());function hc(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;nt(mc(s,n.value),t,5,[s])};return n.value=e,n.attached=pc(),n}function mc(e,t){if(H(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>r=>!r._stopped&&s&&s(r))}else return t}const ni=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,gc=(e,t,n,s,r,o)=>{const a=r==="svg";t==="class"?sc(e,s,a):t==="style"?lc(e,n,s):Xn(t)?Us(t)||uc(e,t,n,s,o):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):vc(e,t,s,a))?(Qr(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Zr(e,t,s,a,o,t!=="value")):e._isVueCE&&(yc(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!ae(s)))?Qr(e,Ae(t),s,o,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),Zr(e,t,s,a))};function vc(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&ni(t)&&K(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return ni(t)&&ae(n)?!1:t in e}function yc(e,t){const n=e._def.props;if(!n)return!1;const s=Ae(t);return Array.isArray(n)?n.some(r=>Ae(r)===s):Object.keys(n).some(r=>Ae(r)===s)}const si=e=>{const t=e.props["onUpdate:modelValue"]||!1;return H(t)?n=>jn(t,n):t};function _c(e){e.target.composing=!0}function ri(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const Is=Symbol("_assign");function ii(e,t,n){return t&&(e=e.trim()),n&&(e=js(e)),e}const bc={created(e,{modifiers:{lazy:t,trim:n,number:s}},r){e[Is]=si(r);const o=s||r.props&&r.props.type==="number";Wt(e,t?"change":"input",a=>{a.target.composing||e[Is](ii(e.value,n,o))}),(n||o)&&Wt(e,"change",()=>{e.value=ii(e.value,n,o)}),t||(Wt(e,"compositionstart",_c),Wt(e,"compositionend",ri),Wt(e,"change",ri))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:s,trim:r,number:o}},a){if(e[Is]=si(a),e.composing)return;const c=(o||e.type==="number")&&!/^0\d/.test(e.value)?js(e.value):e.value,f=t??"";c!==f&&(document.activeElement===e&&e.type!=="range"&&(s&&t===n||r&&e.value.trim()===f)||(e.value=f))}},wc=["ctrl","shift","alt","meta"],xc={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>wc.some(n=>e[`${n}Key`]&&!t.includes(n))},Pn=(e,t)=>{if(!e)return e;const n=e._withMods||(e._withMods={}),s=t.join(".");return n[s]||(n[s]=(r,...o)=>{for(let a=0;a{const n=e._withKeys||(e._withKeys={}),s=t.join(".");return n[s]||(n[s]=r=>{if(!("key"in r))return;const o=Ct(r.key);if(t.some(a=>a===o||Ic[a]===o))return e(r)})},Sc=Ce({patchProp:gc},tc);let oi;function Cc(){return oi||(oi=Pa(Sc))}const Tc=(...e)=>{const t=Cc().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=kc(s);if(!r)return;const o=t._component;!K(o)&&!o.render&&!o.template&&(o.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const a=n(r,!1,$c(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),a},t};function $c(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function kc(e){return ae(e)?document.querySelector(e):e}const Mc={class:"footer"},Rc={__name:"AppFooter",emits:["request-admin"],setup(e,{emit:t}){const n=t,s=oe(0);let r=null;Qs(()=>{r&&window.clearTimeout(r)});function o(){s.value+=1,s.value===1&&(r=window.setTimeout(()=>{s.value=0,r=null},2e3)),s.value>=5&&(r&&(window.clearTimeout(r),r=null),s.value=0,n("request-admin"))}return(a,c)=>(j(),V("div",Mc,[y("div",null,[c[0]||(c[0]=nn(" © 2026 AirShare Pro. All rights reserved. ",-1)),c[1]||(c[1]=y("span",{class:"divider-line"},"|",-1)),y("span",{id:"admin-trigger",title:"点击 5 次进入后台",onClick:o},"V 1.0.0")]),c[2]||(c[2]=y("div",{style:{"font-size":"12px","margin-top":"4px"}},[y("a",{href:"https://beian.miit.gov.cn/",target:"_blank",rel:"noreferrer"}," 粤ICP备2026888888号-1 ")],-1))]))}},Ac=["fill","stroke"],pe={__name:"LocalIcon",props:{name:{type:String,required:!0},size:{type:[Number,String],default:24}},setup(e){const t=e,n={light_mode:{type:"stroke",shapes:[{tag:"circle",attrs:{cx:"12",cy:"12",r:"4"}},{tag:"path",attrs:{d:"M12 2v2.2"}},{tag:"path",attrs:{d:"M12 19.8V22"}},{tag:"path",attrs:{d:"M4.93 4.93 6.5 6.5"}},{tag:"path",attrs:{d:"m17.5 17.5 1.57 1.57"}},{tag:"path",attrs:{d:"M2 12h2.2"}},{tag:"path",attrs:{d:"M19.8 12H22"}},{tag:"path",attrs:{d:"m4.93 19.07 1.57-1.57"}},{tag:"path",attrs:{d:"M17.5 6.5 19.07 4.93"}}]},dark_mode:{type:"fill",shapes:[{tag:"path",attrs:{d:"M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z"}}]},add_circle:{type:"stroke",shapes:[{tag:"circle",attrs:{cx:"12",cy:"12",r:"9"}},{tag:"path",attrs:{d:"M12 8v8"}},{tag:"path",attrs:{d:"M8 12h8"}}]},sensors:{type:"stroke",shapes:[{tag:"path",attrs:{d:"M12 12h.01"}},{tag:"path",attrs:{d:"M9.2 14.8a4 4 0 0 1 0-5.6"}},{tag:"path",attrs:{d:"M14.8 9.2a4 4 0 0 1 0 5.6"}},{tag:"path",attrs:{d:"M6.4 17.6a8 8 0 0 1 0-11.2"}},{tag:"path",attrs:{d:"M17.6 6.4a8 8 0 0 1 0 11.2"}}]},smartphone:{type:"stroke",shapes:[{tag:"rect",attrs:{x:"7",y:"2.5",width:"10",height:"19",rx:"2.5"}},{tag:"path",attrs:{d:"M10 5h4"}},{tag:"circle",attrs:{cx:"12",cy:"18",r:"0.8"}}]},laptop_mac:{type:"stroke",shapes:[{tag:"rect",attrs:{x:"5",y:"4",width:"14",height:"10",rx:"1.5"}},{tag:"path",attrs:{d:"M3 18h18"}},{tag:"path",attrs:{d:"M8 18a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2"}}]},close:{type:"stroke",shapes:[{tag:"path",attrs:{d:"M6 6l12 12"}},{tag:"path",attrs:{d:"M18 6 6 18"}}]},cloud_upload:{type:"stroke",shapes:[{tag:"path",attrs:{d:"M7 18a4 4 0 1 1 .7-7.94A5.5 5.5 0 0 1 18 11a3.5 3.5 0 1 1-.5 7"}},{tag:"path",attrs:{d:"M12 10v8"}},{tag:"path",attrs:{d:"m8.8 13.2 3.2-3.2 3.2 3.2"}}]},arrow_upward:{type:"stroke",shapes:[{tag:"path",attrs:{d:"M12 19V6"}},{tag:"path",attrs:{d:"m6.5 11.5 5.5-5.5 5.5 5.5"}}]},send_and_archive:{type:"stroke",shapes:[{tag:"path",attrs:{d:"M3 6h18l-2 4H5Z"}},{tag:"path",attrs:{d:"M5 10v8a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-8"}},{tag:"path",attrs:{d:"M12 11v5"}},{tag:"path",attrs:{d:"m9.5 13.5 2.5 2.5 2.5-2.5"}}]},chat_bubble:{type:"stroke",shapes:[{tag:"path",attrs:{d:"M6 18.5 3.5 21v-5A7.5 7.5 0 0 1 11 4.5h2A7.5 7.5 0 0 1 20.5 12v.5A7.5 7.5 0 0 1 13 20H8.5"}}]},content_copy:{type:"stroke",shapes:[{tag:"rect",attrs:{x:"9",y:"9",width:"10",height:"10",rx:"2"}},{tag:"path",attrs:{d:"M7 15H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v1"}}]},check:{type:"stroke",shapes:[{tag:"path",attrs:{d:"m5 12 4.2 4.2L19 7.5"}}]},draft:{type:"stroke",shapes:[{tag:"rect",attrs:{x:"4",y:"5",width:"16",height:"14",rx:"2"}},{tag:"path",attrs:{d:"m5 7 7 5 7-5"}}]},save:{type:"stroke",shapes:[{tag:"path",attrs:{d:"M5 20h14a1 1 0 0 0 1-1V7.5L16.5 4H5a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1Z"}},{tag:"path",attrs:{d:"M8 4v5h7"}},{tag:"rect",attrs:{x:"8",y:"14",width:"8",height:"4",rx:"1"}}]},download:{type:"stroke",shapes:[{tag:"path",attrs:{d:"M12 5v10"}},{tag:"path",attrs:{d:"m7.5 10.5 4.5 4.5 4.5-4.5"}},{tag:"path",attrs:{d:"M5 19h14"}}]}},s=It(()=>n[t.name]||n.close),r=It(()=>typeof t.size=="number"?`${t.size}px`:/^\d+(\.\d+)?$/.test(t.size)?`${t.size}px`:t.size);return(o,a)=>(j(),V("span",{class:"app-icon",style:Le({width:r.value,height:r.value}),"aria-hidden":"true"},[(j(),V("svg",{viewBox:"0 0 24 24",fill:s.value.type==="fill"?"currentColor":"none",stroke:s.value.type==="stroke"?"currentColor":"none","stroke-width":"1.8","stroke-linecap":"round","stroke-linejoin":"round"},[(j(!0),V(fe,null,en(s.value.shapes,(c,f)=>(j(),vt(ua(c.tag),go({key:`${e.name}-${f}`},{ref_for:!0},c.attrs),null,16))),128))],8,Ac))],4))}},Ec={class:"header"},Oc={__name:"AppHeader",props:{theme:{type:String,required:!0}},emits:["toggle-theme"],setup(e){return(t,n)=>(j(),V("div",Ec,[n[1]||(n[1]=y("h1",null,"AirShare Pro",-1)),n[2]||(n[2]=y("p",null,"跨端局域网 & P2P 传输中心",-1)),y("button",{class:"theme-toggle",title:"切换日夜模式",onClick:n[0]||(n[0]=s=>t.$emit("toggle-theme"))},[q(pe,{id:"theme-icon",name:e.theme==="dark"?"dark_mode":"light_mode",size:"22"},null,8,["name"])])]))}},Pc={class:"card"},Dc={key:0,class:"section-title"},mn={__name:"GlassCard",props:{title:{type:String,default:""}},setup(e){return(t,n)=>(j(),V("div",Pc,[e.title?(j(),V("div",Dc,te(e.title),1)):Be("",!0),da(t.$slots,"default")]))}},Nc={class:"admin-panel active"},Fc={class:"card admin-header-card"},Uc={class:"transfer-head transfer-head-compact"},Lc={class:"main-grid admin-summary-grid"},Bc={class:"admin-stats-panel"},jc={class:"admin-stats-row"},Hc={class:"admin-fluid-content"},Kc={class:"admin-fluid-icon"},zc={class:"admin-fluid-copy"},Wc={key:0,class:"stat-suffix"},Vc={class:"admin-config-stack"},qc={class:"text-input-group admin-config-row admin-config-row-field admin-config-row-last"},Gc={class:"admin-field-control-row"},Jc=["value"],Yc={class:"text-input-group admin-config-row admin-config-row-field admin-config-row-last"},Xc={class:"admin-field-control-row"},Zc=["value"],Qc={class:"admin-config-insights"},eu={class:"admin-config-highlight"},tu={class:"admin-config-highlight"},nu={class:"admin-table-wrapper"},su={class:"admin-table"},ru={class:"admin-record-type-cell"},iu=["title"],ou={__name:"AdminPanel",props:{stats:{type:Array,required:!0},records:{type:Array,required:!0},fileLimit:{type:Number,required:!0},minioCapacity:{type:Number,required:!0}},emits:["exit","save-config","update:file-limit","update:minio-capacity"],setup(e){function t(a){const c=Number(a)||0;return c>=1024?`${(c/1024).toFixed(c%1024===0?0:1)} GB`:`${c} MB`}function n(a){const c=Number(a)||0;return c>=1024?`${(c/1024).toFixed(c%1024===0?0:1)} TB`:`${c} GB`}function s(a){return a==="blue"?{color:"var(--accent-blue)"}:a==="cyan"?{color:"var(--accent-cyan)"}:a==="success"?{color:"var(--success-green)"}:a==="danger"?{color:"var(--danger-red)"}:{color:"var(--text-main)"}}function r(a){return a==="success"?{color:"var(--success-green)",fontWeight:500}:a==="primary"?{color:"var(--accent-blue)",fontWeight:500}:{color:"var(--danger-red)",fontWeight:500}}function o(a){const c=Number(a)||0;return{"--fluid-level":`${Math.max(0,Math.min(c,100))}%`}}return(a,c)=>(j(),V("div",Nc,[y("div",Fc,[y("div",Uc,[c[5]||(c[5]=y("div",{class:"connected-to"},[y("h2",{class:"admin-title"},"管理控制台"),y("p",{class:"admin-subtitle"},"AirShare Pro System Dashboard")],-1)),y("button",{class:"btn-small-primary",type:"button",onClick:c[0]||(c[0]=f=>a.$emit("exit"))},"退出管理")])]),y("div",Lc,[q(mn,{class:"admin-stats-card",title:"系统运行状态"},{default:Yt(()=>[y("div",Bc,[y("div",jc,[(j(!0),V(fe,null,en(e.stats,f=>(j(),V("div",{key:f.label,class:St(["admin-stat-item",{"admin-stat-item-fluid":f.kind==="minio"}])},[f.kind==="minio"?(j(),V("div",{key:0,class:"admin-fluid-card",style:Le(o(f.percent))},[c[6]||(c[6]=y("div",{class:"admin-fluid-fill"},[y("span",{class:"admin-fluid-wave admin-fluid-wave-a"}),y("span",{class:"admin-fluid-wave admin-fluid-wave-b"})],-1)),y("div",Hc,[y("div",Kc,[q(pe,{name:"save",size:"18"})]),y("div",zc,[y("h3",{style:Le(s(f.tone))},te(f.value),5),y("p",null,te(f.label),1),y("small",null,te(f.detail),1)])])],4)):(j(),V(fe,{key:1},[c[7]||(c[7]=y("span",{class:"admin-stat-kicker"},"实时指标",-1)),y("h3",{style:Le(s(f.tone))},[nn(te(f.value),1),f.suffix?(j(),V("span",Wc,te(f.suffix),1)):Be("",!0)],4),y("p",null,te(f.label),1)],64))],2))),128))])])]),_:1}),q(mn,{class:"admin-config-card",title:"核心参数配置"},{default:Yt(()=>[y("div",Vc,[y("div",qc,[c[8]||(c[8]=y("div",{class:"admin-field-meta"},[y("label",{class:"admin-field-label",for:"admin-file-limit"},"单文件大小限制"),y("p",{class:"admin-field-hint"},"单位为 MB,超过该阈值的文件会按当前后端策略处理。")],-1)),y("div",Gc,[y("input",{id:"admin-file-limit",value:e.fileLimit,min:"1",placeholder:"10240",type:"number",onInput:c[1]||(c[1]=f=>a.$emit("update:file-limit",Number(f.target.value)||0))},null,40,Jc),y("button",{title:"保存配置",type:"button",onClick:c[2]||(c[2]=f=>a.$emit("save-config"))},[q(pe,{name:"save",size:"18"})])])]),y("div",Yc,[c[9]||(c[9]=y("div",{class:"admin-field-meta"},[y("label",{class:"admin-field-label",for:"admin-minio-capacity"},"MinIO 总容量"),y("p",{class:"admin-field-hint"},"单位为 GB,用于容量卡和液位比例计算。")],-1)),y("div",Xc,[y("input",{id:"admin-minio-capacity",value:e.minioCapacity,min:"1",placeholder:"120",type:"number",onInput:c[3]||(c[3]=f=>a.$emit("update:minio-capacity",Number(f.target.value)||0))},null,40,Zc),y("button",{title:"保存配置",type:"button",onClick:c[4]||(c[4]=f=>a.$emit("save-config"))},[q(pe,{name:"save",size:"18"})])])]),y("div",Qc,[y("div",eu,[c[10]||(c[10]=y("span",{class:"admin-config-badge"},"ACTIVE POLICY",-1)),y("h3",null,te(t(e.fileLimit)),1),c[11]||(c[11]=y("p",null,"当前单文件阈值。超过该体积后,文件会按后端已设定的传输与存档策略处理。",-1))]),y("div",tu,[c[12]||(c[12]=y("span",{class:"admin-config-badge"},"MINIO CAPACITY",-1)),y("h3",null,te(n(e.minioCapacity)),1),c[13]||(c[13]=y("p",null,"当前 MinIO 总容量基线,用于后台容量展示与液位占比计算。",-1))])])])]),_:1})]),q(mn,{class:"admin-table-card",title:"最近传输记录(Top 5)"},{default:Yt(()=>[y("div",nu,[y("table",su,[c[14]||(c[14]=y("thead",null,[y("tr",null,[y("th",null,"时间"),y("th",null,"发送端特征"),y("th",null,"传输类型"),y("th",null,"数据量"),y("th",null,"状态")])],-1)),y("tbody",null,[(j(!0),V(fe,null,en(e.records,f=>(j(),V("tr",{key:`${f.time}-${f.peer}`},[y("td",null,te(f.time),1),y("td",null,te(f.peer),1),y("td",ru,[y("span",{class:"admin-record-type",title:f.type},te(f.type),9,iu)]),y("td",null,te(f.size),1),y("td",{style:Le(r(f.tone))},te(f.status),5)]))),128))])])])]),_:1})]))}},lu={class:"local-device-name"},au={key:0,class:"radar-container"},cu={class:"radar"},uu={key:1,class:"device-list"},fu=["onClick"],du={class:"device-icon"},pu={class:"device-info"},hu={key:2,class:"radar-container"},mu={class:"radar"},gu={__name:"DeviceDiscoveryCard",props:{isScanning:{type:Boolean,required:!0},localDeviceName:{type:String,default:""},devices:{type:Array,required:!0}},emits:["select-device"],setup(e,{emit:t}){const n=t;function s(r){n("select-device",r)}return(r,o)=>(j(),vt(mn,{title:"局域网自动发现"},{default:Yt(()=>[y("p",lu,[o[0]||(o[0]=nn(" 本机:",-1)),y("strong",null,te(e.localDeviceName||"识别中"),1)]),e.isScanning?(j(),V("div",au,[y("div",cu,[q(pe,{class:"radar-icon",name:"sensors",size:"36"})]),o[1]||(o[1]=y("p",{class:"scan-status"},"正在扫描附近设备...",-1))])):e.devices.length?(j(),V("div",uu,[(j(!0),V(fe,null,en(e.devices,a=>(j(),V("button",{key:a.id,class:"device-item",type:"button",onClick:c=>s(a)},[y("div",du,[q(pe,{name:a.icon,size:"24"},null,8,["name"])]),y("div",pu,[y("h4",null,te(a.name),1),y("p",null,te(a.description),1)]),o[2]||(o[2]=y("div",{class:"device-status-beacon","aria-hidden":"true"},[y("span",{class:"device-status-dot"}),y("span",{class:"device-status-ring"}),y("span",{class:"device-status-ring device-status-ring-delay"})],-1))],8,fu))),128))])):(j(),V("div",hu,[y("div",mu,[q(pe,{class:"radar-icon",name:"devices",size:"36"})]),o[3]||(o[3]=y("p",{class:"scan-status"},"暂未发现局域网设备,请保持页面开启后重试",-1))]))]),_:1}))}},vu={key:0,class:"room-action-area"},yu={class:"room-input-group"},_u=["value"],bu={key:0,class:"pending-downloads"},wu={class:"pending-downloads-head"},xu=["href"],Iu={class:"pending-download-copy"},Su=["title"],Cu={class:"pending-download-icon","aria-hidden":"true"},Tu={key:1,class:"waiting-area"},$u={class:"huge-code"},ku={__name:"RemoteRoomCard",props:{roomCodeInput:{type:String,required:!0},isWaiting:{type:Boolean,required:!0},generatedCode:{type:String,required:!0},pendingDownloads:{type:Array,required:!0}},emits:["update-room-code","create-room","join-room","cancel-room"],setup(e,{emit:t}){const n=t;function s(o){n("update-room-code",o.target.value)}function r(){n("join-room")}return(o,a)=>(j(),vt(mn,{title:"远程直连"},{default:Yt(()=>[e.isWaiting?(j(),V("div",Tu,[a[6]||(a[6]=y("p",{class:"waiting-subtitle"},"您的房间号码",-1)),y("div",$u,te(e.generatedCode),1),a[7]||(a[7]=y("div",{class:"spinner"},null,-1)),a[8]||(a[8]=y("p",{class:"waiting-tip"},"等待对方加入...",-1)),y("button",{class:"btn-cancel",type:"button",onClick:a[2]||(a[2]=c=>o.$emit("cancel-room"))},"取消建房")])):(j(),V("div",vu,[y("button",{class:"btn-create",type:"button",onClick:a[0]||(a[0]=c=>o.$emit("create-room"))},[q(pe,{name:"add_circle",size:"22"}),a[3]||(a[3]=nn(" 创建专属传输房间 ",-1))]),a[5]||(a[5]=y("div",{class:"divider"},"或",-1)),y("div",yu,[y("input",{class:"room-code",inputmode:"numeric",maxlength:"4",pattern:"\\d*",placeholder:"输入4位房间号",type:"text",value:e.roomCodeInput,onInput:s,onKeyup:bo(r,["enter"])},null,40,_u),y("button",{class:"btn-primary",type:"button",onClick:a[1]||(a[1]=c=>o.$emit("join-room"))},"加入房间")]),e.pendingDownloads.length?(j(),V("div",bu,[y("div",wu,[a[4]||(a[4]=y("span",null,"待领取文件",-1)),y("span",null,te(e.pendingDownloads.length),1)]),(j(!0),V(fe,null,en(e.pendingDownloads,c=>(j(),V("a",{key:c.transfer_id,class:"pending-download-item",href:c.download_path,target:"_blank",rel:"noopener noreferrer"},[y("div",Iu,[y("strong",{title:c.name},te(c.name),9,Su),y("p",null,te(c.size_label)+" · "+te(c.created_label),1)]),y("span",Cu,[q(pe,{name:"download",size:"18"})])],8,xu))),128))])):Be("",!0)]))]),_:1}))}},Mu={class:"file-info"},Ru=["title"],Au={class:"file-info-right"},Eu=["download","href"],Ou={key:0,class:"progress-bar-container"},Pu={__name:"TransferQueueItem",props:{item:{type:Object,required:!0}},emits:["remove","start-upload","copy"],setup(e){const t=e,n=It(()=>t.item.tone==="success"?{color:"var(--success-green)"}:t.item.tone==="primary"?{color:"var(--accent-blue)"}:t.item.tone==="danger"?{color:"var(--danger-red)"}:{color:"var(--text-secondary)"}),s=It(()=>t.item.kind==="text"?{color:"var(--success-green)",background:"rgba(48, 209, 88, 0.1)"}:{});return(r,o)=>(j(),V("div",{class:St(["batch-item",{"pending-file":e.item.kind==="file"&&e.item.pending}])},[y("div",Mu,[y("div",{class:"file-info-left",style:Le(e.item.kind==="text"?{maxWidth:"70%"}:null)},[y("div",{class:"file-icon-wrapper",style:Le(s.value)},[q(pe,{name:e.item.kind==="text"?"chat_bubble":"draft",size:"18"},null,8,["name"])],4),y("span",{class:"file-name",title:e.item.kind==="text"?e.item.text:e.item.name},te(e.item.kind==="text"?e.item.text:e.item.name),9,Ru)],4),y("div",Au,[y("span",{class:"file-status",style:Le(n.value)},te(e.item.kind==="text"&&e.item.copied?"已复制":e.item.status),5),e.item.kind==="text"?(j(),V("button",{key:0,class:"action-btn",title:"复制文本",type:"button",onClick:o[0]||(o[0]=a=>r.$emit("copy",e.item.id))},[q(pe,{name:e.item.copied?"check":"content_copy",size:"16"},null,8,["name"])])):Be("",!0),e.item.kind==="file"&&e.item.pending?(j(),V("button",{key:1,class:"action-btn primary",title:"发送文件",type:"button",onClick:o[1]||(o[1]=a=>r.$emit("start-upload",e.item.id))},[q(pe,{name:"arrow_upward",size:"16"})])):Be("",!0),e.item.kind==="file"&&e.item.downloadUrl?(j(),V("a",{key:2,class:"action-btn primary",download:e.item.name,href:e.item.downloadUrl,title:"保存文件"},[q(pe,{name:"download",size:"16"})],8,Eu)):Be("",!0),y("button",{class:"action-btn danger",title:"移除记录",type:"button",onClick:o[2]||(o[2]=a=>r.$emit("remove",e.item.id))},[q(pe,{name:"close",size:"16"})])])]),e.item.kind==="file"?(j(),V("div",Ou,[y("div",{class:St(["progress-bar-fill",{success:e.item.tone==="success"}]),style:Le({width:`${e.item.progress}%`})},null,6)])):Be("",!0)],2))}},Du={class:"transfer-panel active"},Nu={class:"card"},Fu={class:"transfer-head"},Uu={class:"connected-to"},Lu={key:0,class:"connection-hint"},Bu={class:"text-input-group"},ju={__name:"TransferPanel",props:{peerName:{type:String,required:!0},connectionType:{type:String,required:!0},networkHint:{type:String,default:""},items:{type:Array,required:!0},hasPendingItems:{type:Boolean,required:!0}},emits:["close","send-text","files-selected","send-all-pending","remove-item","start-upload","copy-item"],setup(e,{emit:t}){const n=e,s=t,r=oe(""),o=oe(!1),a=oe(null),c=oe(null);Ut(()=>n.items.length,async()=>{await Ni(),a.value&&(a.value.scrollTop=a.value.scrollHeight)});function f(){var $;($=c.value)==null||$.click()}function m(){s("send-text",r.value),r.value=""}function p($){const S=Array.from($.target.files||[]);S.length&&s("files-selected",S),$.target.value=""}function _($){var U;o.value=!1;const S=Array.from(((U=$.dataTransfer)==null?void 0:U.files)||[]);S.length&&s("files-selected",S)}return($,S)=>(j(),V("div",Du,[y("div",Nu,[y("div",Fu,[y("div",Uu,[y("h2",null,te(e.peerName),1),y("p",null,te(e.connectionType),1),e.networkHint?(j(),V("small",Lu,te(e.networkHint),1)):Be("",!0)]),y("button",{class:"close-btn",type:"button",onClick:S[0]||(S[0]=U=>$.$emit("close"))},[q(pe,{name:"close",size:"20"})])]),y("div",{class:St(["drop-zone",{"drop-zone-active":o.value}]),onClick:f,onDragenter:S[1]||(S[1]=Pn(U=>o.value=!0,["prevent"])),onDragover:S[2]||(S[2]=Pn(U=>o.value=!0,["prevent"])),onDragleave:S[3]||(S[3]=Pn(U=>o.value=!1,["prevent"])),onDrop:Pn(_,["prevent"])},[q(pe,{class:"drop-zone-icon",name:"cloud_upload",size:"42"}),S[9]||(S[9]=y("p",{class:"drop-zone-text"},"点击或拖拽多个文件到这里",-1)),y("input",{ref_key:"fileInput",ref:c,class:"hidden",multiple:"",type:"file",onChange:p},null,544)],34),y("div",Bu,[Wl(y("input",{"onUpdate:modelValue":S[4]||(S[4]=U=>r.value=U),placeholder:"输入要发送的文本或链接...",type:"text",onKeyup:bo(m,["enter"])},null,544),[[bc,r.value]]),y("button",{title:"发送文本",type:"button",onClick:m},[q(pe,{name:"arrow_upward",size:"20"})])]),y("div",{class:St(["batch-actions",{active:e.hasPendingItems}])},[y("button",{class:"btn-small-primary",type:"button",onClick:S[5]||(S[5]=U=>$.$emit("send-all-pending"))},[q(pe,{name:"send_and_archive",size:"16"}),S[10]||(S[10]=nn(" 一键发送全部 ",-1))])],2),e.items.length?(j(),V("div",{key:0,ref_key:"batchContainer",ref:a,class:"batch-progress-container"},[(j(!0),V(fe,null,en(e.items,U=>(j(),vt(Pu,{key:U.id,item:U,onCopy:S[6]||(S[6]=N=>$.$emit("copy-item",N)),onRemove:S[7]||(S[7]=N=>$.$emit("remove-item",N)),onStartUpload:S[8]||(S[8]=N=>$.$emit("start-upload",N))},null,8,["item"]))),128))],512)):Be("",!0)])]))}};let Vt={deviceId:"",token:""};const Hu="filefast_device_id",Ku="filefast_device_token";function wo(){return!Vt.deviceId||!Vt.token?{}:{"X-Device-ID":Vt.deviceId,"X-Device-Token":Vt.token}}function zu(e={},t=!1){return{...t?{"Content-Type":"application/json"}:{},...wo(),...e}}function Wu(e,t){if(!t||Object.keys(t).length===0)return e;const n=new URLSearchParams;Object.entries(t).forEach(([r,o])=>{o!=null&&o!==""&&n.set(r,String(o))});const s=n.toString();return s?`${e}?${s}`:e}async function Dn(e,t={}){const n=t.body!==void 0,s=await fetch(Wu(e,t.query),{method:t.method||"GET",headers:zu(t.headers,n),body:n?JSON.stringify(t.body):void 0}),r=await s.json().catch(()=>({}));if(!s.ok){const o=new Error(r.error||`Request failed: ${s.status}`);throw o.status=s.status,o}return r.data}const ve={get(e,t={}){return Dn(e,{...t,method:"GET"})},post(e,t,n={}){return Dn(e,{...n,method:"POST",body:t})},put(e,t,n={}){return Dn(e,{...n,method:"PUT",body:t})},patch(e,t,n={}){return Dn(e,{...n,method:"PATCH",body:t})}};function li(e,t){Vt={deviceId:e||"",token:t||""},Gu(Vt)}function Vu(){return wo()}function qu(e){return{Authorization:`Bearer ${e}`}}function Gu(e){typeof document>"u"||(ai(Hu,e.deviceId),ai(Ku,e.token))}function ai(e,t){if(!t){document.cookie=`${e}=; Path=/; Max-Age=0; SameSite=Lax`;return}document.cookie=`${e}=${encodeURIComponent(t)}; Path=/; SameSite=Lax`}function Nn(e){return{headers:qu(e)}}const Kt={login(e,t){return ve.post("/api/admin/login",{username:e,password:t})},stats(e){return ve.get("/api/admin/stats",Nn(e))},config(e){return ve.get("/api/admin/config",Nn(e))},updateConfig(e,t){return ve.put("/api/admin/config",t,Nn(e))},recentTransfers(e){return ve.get("/api/admin/transfers/recent",Nn(e))}},Fn={register(e){return ve.post("/api/devices/register",e)},heartbeat(e){return ve.post("/api/devices/heartbeat",{device_id:e})},listCandidates(e){return ve.get("/api/devices/candidates",{query:{deviceId:e}})},listPendingDownloads(e){return ve.get(`/api/devices/${encodeURIComponent(e)}/pending-downloads`)}},Un={create(e){return ve.post("/api/rooms",{creator_device_id:e})},get(e){return ve.get(`/api/rooms/${encodeURIComponent(e)}`)},join(e,t){return ve.post("/api/rooms/join",{code:e,joiner_device_id:t})},cancel(e,t){return ve.post(`/api/rooms/${encodeURIComponent(e)}/cancel`,{requester_id:t})}},Ju={config(){return ve.get("/api/runtime/config")}},ge={create(e){return ve.post("/api/transfers",e)},presignFallback(e){return ve.post(`/api/transfers/${encodeURIComponent(e)}/fallback/presign`,{})},uploadFallback(e,t,n){return Yu(`/api/transfers/${encodeURIComponent(e)}/fallback/upload`,t,n)},updateStatus(e,t){return ve.patch(`/api/transfers/${encodeURIComponent(e)}/status`,t)}};function Yu(e,t,n){return new Promise((s,r)=>{const o=new XMLHttpRequest;o.open("PUT",e),o.responseType="json",o.setRequestHeader("Content-Type",t.type||"application/octet-stream"),Object.entries(Vu()).forEach(([a,c])=>{o.setRequestHeader(a,c)}),o.upload.onprogress=a=>{!a.lengthComputable||typeof n!="function"||n(Math.round(a.loaded/a.total*100))},o.onload=()=>{const a=o.response||Xu(o.responseText);if(o.status>=200&&o.status<300){s(a.data);return}r(new Error((a==null?void 0:a.error)||`Upload failed: ${o.status}`))},o.onerror=()=>r(new Error("Upload failed")),o.send(t)})}function Xu(e){try{return JSON.parse(e)}catch{return null}}const Zu={class:"container"},Qu={key:0,class:"main-grid"},Ss="filefast-admin-token",Ln="filefast-admin-view",Bn="filefast-device-id",ci="filefast-device-name",Cs="filefast-device-token",ef=15e3,tf=5e3,nf=2e3,sf=3e3,rf=4*1024*1024,of=2e4,lf=16*1024,ui=512*1024,af={__name:"App",setup(e){const t=oe(localStorage.getItem("airshare-theme")||"light"),n=oe(localStorage.getItem(Ln)==="admin"?"admin":"main"),s=oe(!0),r=oe([]),o=oe(""),a=oe(!1),c=oe("----"),f=oe([]),m=oe({name:"--",type:"等待连接",deviceId:"",networkGroupKey:""}),p=oe([]),_=oe("/ws"),$=oe(10240),S=oe(120),U=oe([]),N=oe([]),Y=oe(null),z=oe(localStorage.getItem(Ss)||""),A=oe({id:"",name:"",type:"",networkGroupKey:""}),Q=localStorage.getItem(Bn)||"",F=localStorage.getItem(Cs)||"";Q&&F&&li(Q,F);const le=new Map,he=new Map,Te=new Map,$e=new Map;let _t=null,He=null,Je=null,Ke=null,bt=null,de=null,st=null,L=null,B=null,G="",_e="p2p",ze=!1,be=!1,ue=!1,wt=null,Tt=null;const Sn=It(()=>p.value.filter(i=>i.kind==="file"&&i.pending)),$t=It(()=>Sn.value.length>0),Lt=It(()=>!m.value.deviceId||ar(m.value.networkGroupKey)||Uo()?"":"当前是跨网络访问,未配置 TURN 时实时通道可能失败。文本和小文件可回退中转,大文件建议使用 MinIO。");Ut(t,i=>{document.body.setAttribute("data-theme",i),localStorage.setItem("airshare-theme",i)},{immediate:!0}),Ut(n,i=>{if(i==="admin"&&z.value){localStorage.setItem(Ln,"admin");return}localStorage.removeItem(Ln)}),Ut([n,z],([i,l])=>{Ke&&(window.clearInterval(Ke),Ke=null),!(i!=="admin"||!l)&&(Ke=window.setInterval(()=>{ls().catch(u=>{console.error(u)})},5e3))}),Vi(async()=>{_.value=Fo(),await sn(),n.value==="admin"&&z.value&&ls().catch(i=>{console.error(i)}),He=window.setInterval(()=>{w()},ef),_t=window.setInterval(()=>{v()},tf),bt=window.setInterval(()=>{I()},1e4)}),Qs(()=>{_t&&window.clearInterval(_t),He&&window.clearInterval(He),Ke&&window.clearInterval(Ke),bt&&window.clearInterval(bt),b(),ot(),yr(),D()});async function sn(){try{await kt(),await g(),await v()}catch(i){window.alert(`后端连接失败:${i.message}`)}}function Cn(){t.value=t.value==="dark"?"light":"dark"}async function kt(){try{Tn(await Ju.config())}catch(i){console.error(i)}}function Tn(i){i&&(Y.value=i,$.value=Math.round((i.max_minio_fallback_size_bytes||0)/1024/1024),S.value=Math.max(0,Math.round((i.minio_capacity_bytes||0)/1024/1024/1024)))}function d(i){o.value=i.replace(/\D/g,"").slice(0,4)}async function g(){const i=Ao(),l=Eo(i),u=Do(),h=await Fn.register({device_id:i,name:l,type:u,network_group_key:lr()});localStorage.setItem(Bn,h.id),h.auth_token&&(localStorage.setItem(Cs,h.auth_token),li(h.id,h.auth_token)),A.value={id:h.id,name:h.name,type:h.type,networkGroupKey:h.network_group_key||""},await I(),vr()}async function v(){if(A.value.id)try{const i=await ge.create({kind:"text",name:"text-message",content:value,sender_device_id:A.value.id,receiver_device_id:m.value.deviceId});try{await rn(i,value)}catch(l){console.warn("realtime text send failed, fallback to relay",l),await rr(i,value)}p.value.push({id:Ue("text"),transferId:i.id,kind:"text",text:value,status:"已发送",tone:"success",copied:!1})}catch(i){window.alert(`发送文本失败:${i.message}`)}}async function I(){if(!A.value.id){f.value=[];return}try{const i=await Fn.listPendingDownloads(A.value.id);f.value=i.map(l=>({...l,name:rt(l.name),download_path:l.download_path||`/api/transfers/${encodeURIComponent(l.transfer_id)}/fallback/download`,size_label:At(Number(l.size_bytes||0)),created_label:Rn(l.created_at)}))}catch(i){if((i==null?void 0:i.status)===404){f.value=[];return}console.error(i)}}async function w(){if(A.value.id)try{await Fn.heartbeat(A.value.id)}catch(i){console.error(i)}}async function x(){if(!A.value.id){window.alert("当前设备尚未注册到后端");return}try{const i=await Un.create(A.value.id);c.value=i.code,a.value=!0,C(i.code)}catch(i){window.alert(`创建房间失败:${i.message}`)}}async function R(){const i=c.value;b();try{a.value&&i!=="----"&&await Un.cancel(i,A.value.id)}catch(l){console.error(l)}finally{a.value=!1,c.value="----"}}async function k(){if(!(o.value.length<4))try{const i=await Un.join(o.value,A.value.id),l=Rt(i.creator_device_id);o.value="",P({deviceId:i.creator_device_id,name:(l==null?void 0:l.name)||`房间 ${i.code} 创建者`,type:"房间配对成功"})}catch(i){window.alert(`加入房间失败:${i.message}`)}}function C(i){b(),Je=window.setInterval(async()=>{try{const l=await Un.get(i);if(l.status==="joined"&&l.joiner_device_id){const u=Rt(l.joiner_device_id);P({deviceId:l.joiner_device_id,name:(u==null?void 0:u.name)||`房间 ${i} 对端`,type:"房间配对成功"});return}(l.status==="expired"||l.status==="canceled")&&(b(),a.value=!1,c.value="----")}catch(l){console.error(l)}},nf)}function b(){Je&&(window.clearInterval(Je),Je=null)}function P(i){const l=i.deviceId||i.id||"",u=i.connectionType||i.type||"点对点传输";b(),m.value.deviceId!==l&&(ot(),D()),m.value={name:i.name,type:i.connectionType||i.type||"点对点传输",deviceId:i.deviceId||i.id||"",networkGroupKey:i.network_group_key||""},a.value=!1,c.value="----",n.value="transfer",m.value.baseType=u,m.value.type=u,m.value.deviceId=l,We("正在建立实时通道"),it(l,{initiate:!0})}function M(i,l=!1){const u=i.deviceId||i.id||"",h=i.connectionType||i.type||"点对点传输";m.value.deviceId===u&&n.value==="transfer"||(ot(),l||D()),m.value={name:i.name,type:i.connectionType||i.type||"点对点传输",deviceId:u,networkGroupKey:i.network_group_key||""},a.value=!1,c.value="----",n.value="transfer",m.value.baseType=h,m.value.type=h,m.value.deviceId=u,We("正在建立实时通道"),it(u)}function O(){ot(),D(),m.value={name:"--",type:"等待连接",deviceId:"",networkGroupKey:""},n.value="main",m.value.baseType="等待连接",m.value.type="等待连接"}function D(){p.value.forEach(i=>Pe(i)),p.value=[],he.clear()}async function W(i){const l=i.trim();if(l){if(!m.value.deviceId){window.alert("当前没有可用的接收端");return}try{const u=await ge.create({kind:"text",name:"text-message",content:l,sender_device_id:A.value.id,receiver_device_id:m.value.deviceId});ce("transfer.created",m.value.deviceId,{transfer_id:u.id,kind:"text",name:"text-message",content:l,sender_device_id:A.value.id,sender_name:A.value.name,sender_type:A.value.type,receiver_device_id:m.value.deviceId,final_status:"completed",current_channel:"p2p",transport_options:as()}),await ge.updateStatus(u.id,{current_channel:"p2p",final_status:"completed"}),ce("transfer.updated",m.value.deviceId,{transfer_id:u.id,final_status:"completed",current_channel:"p2p"}),p.value.push({id:Ue("text"),transferId:u.id,kind:"text",text:l,status:"已发送",tone:"success",copied:!1})}catch(u){window.alert(`发送文本失败:${u.message}`)}}}function ee(i){const l=i.filter(Boolean).map((u,h)=>({id:Ue(`file-${h}`),kind:"file",file:u,name:rt(u.name),size:At(u.size),sizeBytes:u.size,status:"待发送",tone:"muted",progress:0,pending:!0,transferId:""}));l.length&&p.value.push(...l)}async function J(i){const l=p.value.find(u=>u.id===i);if(!(!l||l.kind!=="file"||!l.pending)){if(!m.value.deviceId){window.alert("当前没有可用的接收端");return}l.pending=!1,l.status="创建传输中...",l.tone="primary";try{const u=await ge.create({kind:"file",name:l.name,size_bytes:l.sizeBytes,sender_device_id:A.value.id,receiver_device_id:m.value.deviceId});if(l.transferId=u.id,l.sizeBytes>rf){await me(l,u);return}ce("transfer.created",m.value.deviceId,{transfer_id:u.id,kind:"file",name:l.name,size_bytes:l.sizeBytes,sender_device_id:A.value.id,sender_name:A.value.name,sender_type:A.value.type,receiver_device_id:m.value.deviceId,final_status:"connecting",current_channel:"p2p",transport_options:as()});try{await ir(l,u)}catch(h){console.warn("realtime file send failed, fallback to relay",h),await or(l,u)}}catch(u){l.pending=!0,l.status=`发送失败:${u.message}`,l.tone="danger"}}}async function me(i,l){i.progress=0,i.status="上传准备中...";try{if(!l.fallback_allowed)throw new Error("当前文件过大,且未启用 MinIO 回退");await ge.presignFallback(i.transferId),ce("transfer.updated",m.value.deviceId,{transfer_id:i.transferId,final_status:"fallback_uploading",current_channel:"minio"}),i.status="上传中...";const u=await ge.uploadFallback(i.transferId,i.file,h=>{i.progress=Math.max(1,Math.min(h,99))});await ge.updateStatus(i.transferId,{current_channel:"minio",final_status:"completed"}),ce("transfer.updated",m.value.deviceId,{transfer_id:i.transferId,final_status:"completed",current_channel:"minio"}),ce("transfer.file",m.value.deviceId,{transfer_id:i.transferId,name:i.name,download_url:u.download_path||u.download_url}),i.progress=100,i.status="上传完成",i.tone="success"}catch(u){i.pending=!0,i.status=`上传失败:${u.message}`,i.tone="danger"}}async function we(){for(const i of Sn.value)await J(i.id)}async function Ne(i){const l=p.value.find(u=>u.id===i);if(l&&Pe(l),p.value=p.value.filter(u=>u.id!==i),!(!(l!=null&&l.transferId)||l.tone==="success"))try{await ge.updateStatus(l.transferId,{final_status:"cancelled"}),ce("transfer.updated",m.value.deviceId,{transfer_id:l.transferId,final_status:"cancelled"})}catch(u){console.error(u)}}async function Fe(i){const l=p.value.find(u=>u.id===i);if(!(!l||l.kind!=="text"))try{await Mt(l.text),l.copied=!0,window.setTimeout(()=>{const u=p.value.find(h=>h.id===i);u&&u.kind==="text"&&(u.copied=!1)},2e3)}catch{window.alert("复制失败")}}async function Mt(i){var u;if((u=navigator.clipboard)!=null&&u.writeText&&window.isSecureContext){await navigator.clipboard.writeText(i);return}const l=document.createElement("textarea");l.value=i,l.setAttribute("readonly","readonly"),l.style.position="fixed",l.style.top="0",l.style.left="-9999px",l.style.opacity="0",document.body.appendChild(l),l.focus(),l.select(),l.setSelectionRange(0,l.value.length);try{if(!document.execCommand("copy"))throw new Error("copy command failed")}finally{document.body.removeChild(l)}}function $n(i){const l=le.get(i);l&&(window.clearInterval(l),le.delete(i))}function ke(i){return new Promise((l,u)=>{const h=new FileReader;h.onload=()=>l(String(h.result||"")),h.onerror=()=>u(new Error("Failed to read file")),h.readAsDataURL(i)})}function Pe(i){if($n(i.id),i.ownedDownloadUrl&&i.downloadUrl)try{URL.revokeObjectURL(i.downloadUrl)}catch(l){console.error(l)}i.transferId&&he.delete(i.transferId)}function Bt(i,l,u=!1){if(i.ownedDownloadUrl&&i.downloadUrl&&i.downloadUrl!==l)try{URL.revokeObjectURL(i.downloadUrl)}catch(h){console.error(h)}i.downloadUrl=l,i.ownedDownloadUrl=u}async function rn(i,l){const u=await dr(m.value.deviceId);kn(u,{type:"text",transfer_id:i.id,text:l,sender_device_id:A.value.id,sender_name:A.value.name,sender_type:A.value.type});const h=gr();await ge.updateStatus(i.id,{current_channel:h,final_status:"completed"})}async function rr(i,l){ce("transfer.created",m.value.deviceId,{transfer_id:i.id,kind:"text",name:"text-message",content:l,sender_device_id:A.value.id,sender_name:A.value.name,sender_type:A.value.type,receiver_device_id:m.value.deviceId,final_status:"completed",current_channel:"p2p"}),await ge.updateStatus(i.id,{current_channel:"p2p",final_status:"completed"})}async function ir(i,l){var E;const u=await dr(m.value.deviceId);i.status="正在通过 WebRTC 发送...",i.progress=1,kn(u,{type:"file-meta",transfer_id:l.id,name:i.name,mime_type:((E=i.file)==null?void 0:E.type)||"application/octet-stream",size_bytes:i.sizeBytes,sender_device_id:A.value.id,sender_name:A.value.name,sender_type:A.value.type});let h=0;for(;hui;)await To(20)}function To(i){return new Promise(l=>{window.setTimeout(l,i)})}function $o(i,l,u){return new Promise((h,T)=>{const E=window.setTimeout(()=>{T(new Error(u))},l);i.then(se=>{window.clearTimeout(E),h(se)}).catch(se=>{window.clearTimeout(E),T(se)})})}async function ko(){const i=window.prompt("管理员用户名","admin");if(i===null)return;const l=window.prompt("管理员密码");if(l!==null)try{const u=await Kt.login(i.trim()||"admin",l);z.value=u.token,localStorage.setItem(Ss,u.token),await ls(),n.value="admin"}catch(u){window.alert(`管理员登录失败:${u.message}`)}}function Mo(){n.value="main"}async function ls(){if(z.value)try{const[i,l,u]=await Promise.all([Kt.stats(z.value),Kt.config(z.value),Kt.recentTransfers(z.value)]);Tn(l),U.value=xr(i.stats||{},i.minio||{}),N.value=u.map(h=>Ir(h))}catch(i){throw(i==null?void 0:i.status)===401&&(localStorage.removeItem(Ss),localStorage.removeItem(Ln),z.value="",n.value="main"),i}}async function Ro(){if(!z.value||!Y.value){window.alert("当前没有可用的管理员会话");return}try{const i={...Y.value,max_minio_fallback_size_bytes:Math.max(0,$.value)*1024*1024,minio_capacity_bytes:Math.max(0,S.value)*1024*1024*1024},l=await Kt.updateConfig(z.value,i);Tn(l);{const u=await Kt.stats(z.value);U.value=xr(u.stats||{},u.minio||{})}window.alert("配置已保存")}catch(i){window.alert(`保存配置失败:${i.message}`)}}function Ao(){let i=localStorage.getItem(Bn);return i||(i=typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():`web-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,localStorage.setItem(Bn,i)),i}function Eo(i){let l=localStorage.getItem(ci);return(!l||Oo(l,i))&&(l=`${Po()} ${i.slice(0,4)}`,localStorage.setItem(ci,l)),l}function Oo(i,l){const u=String(i||"").trim(),h=l.slice(0,4);return!u||!h||!u.endsWith(` ${h}`)?!1:/^(android|iphone|ipad|linux|macintel|macos|windows|win32|web)\s/i.test(u)}function Po(){const i=`${navigator.userAgent} ${navigator.platform}`.toLowerCase();return i.includes("iphone")?"iPhone":i.includes("ipad")?"iPad":i.includes("android")?"Android":i.includes("windows")||i.includes("win32")?"Windows":i.includes("mac os")||i.includes("macintosh")||i.includes("macintel")?"macOS":i.includes("linux")?"Linux":"Web"}function Do(){const i=`${navigator.userAgent} ${navigator.platform}`.toLowerCase();return i.includes("iphone")||i.includes("android")||i.includes("mobile")?"phone":i.includes("ipad")||i.includes("tablet")?"tablet":"desktop"}function rt(i,l="file"){const u=String(i||"").trim();if(!u)return l;if(!/%[0-9A-Fa-f]{2}/.test(u))return u;try{return decodeURIComponent(u)}catch{return u}}function No(i){return i==="phone"?"smartphone":i==="tablet"?"tablet_mac":"laptop_mac"}function jt(i){return i==="phone"?"手机":i==="tablet"?"平板":"桌面端"}function Fo(){return`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/ws`}function Uo(){var i;return Array.isArray((i=Y.value)==null?void 0:i.turn_urls)&&Y.value.turn_urls.some(l=>String(l||"").trim())}function lr(){const i=String(window.location.hostname||"").trim().toLowerCase();return i?i==="localhost"||i==="127.0.0.1"||i==="::1"||i.endsWith(".local")||Lo(i)?i:"":"local"}function ar(i){const l=A.value.networkGroupKey||lr();return!!l&&i===l}function Lo(i){const l=i.split(".");if(l.length!==4||l.some(T=>!/^\d+$/.test(T)))return!1;const[u,h]=l.map(T=>Number(T));return u===10||u===127||u===192&&h===168?!0:u===172&&h>=16&&h<=31}function cr(i){const l=Array.isArray(i==null?void 0:i.turn_urls)?i.turn_urls.map(u=>String(u||"").trim()).filter(Boolean):[];return l.length?[{urls:l,username:(i==null?void 0:i.turn_username)||"",credential:(i==null?void 0:i.turn_password)||""}]:[]}function as(){var i,l;return{ice_servers:cr(Y.value),p2p_connect_timeout_sec:((i=Y.value)==null?void 0:i.p2p_connect_timeout_sec)||15,turn_connect_timeout_sec:((l=Y.value)==null?void 0:l.turn_connect_timeout_sec)||20}}function ur(){return typeof RTCPeerConnection<"u"}function cs(){wt=null,Tt=null}function Bo(){return wt||(wt=new Promise(i=>{Tt=i})),wt}function fr(i){Tt&&Tt(i),wt=Promise.resolve(i),Tt=null}function We(i=""){if(!m.value.deviceId)return;const l=m.value.baseType||m.value.type||"点对点传输";m.value={...m.value,type:i?`${l} · ${i}`:l}}function jo(i){return!L||G!==i||L.signalingState==="closed"||["failed","disconnected","closed"].includes(L.connectionState)||["failed","disconnected","closed"].includes(L.iceConnectionState)?!0:!B||B.readyState==="closed"}async function it(i,l={}){return!i||!ur()?null:(jo(i)&&(ot(),Ho(i)),l.initiate&&L.signalingState==="stable"&&await Ko(i),L)}function Ho(i){G=i,_e="p2p",ze=!1,be=!1,ue=!1,$e.delete(i),cs(),L=new RTCPeerConnection({iceServers:cr(Y.value)}),B=L.createDataChannel("filefast-control",{negotiated:!0,id:0,ordered:!0}),zo(B),L.onicecandidate=({candidate:l})=>{if(l)try{ce("webrtc.candidate",i,{candidate:l})}catch(u){console.error(u)}},L.onconnectionstatechange=()=>{if(L){if(us(),L.connectionState==="connected"){We(_e==="turn"?"TURN 中继已连接":"WebRTC 直连已连接");return}if(L.connectionState==="connecting"){We("实时通道连接中");return}if(L.connectionState==="failed"){We("实时通道连接失败");return}(L.connectionState==="disconnected"||L.connectionState==="closed")&&We("实时通道已断开")}},L.oniceconnectionstatechange=()=>{us()}}async function Ko(i){if(L)try{ze=!0,await L.setLocalDescription(),ce("webrtc.description",i,{description:L.localDescription})}finally{ze=!1}}function zo(i){B=i,i.bufferedAmountLowThreshold=ui/2,i.onopen=()=>{fr(i),We(_e==="turn"?"TURN 中继已连接":"WebRTC 直连已连接"),us()},i.onclose=()=>{B===i&&(B=null,cs(),We("实时通道已关闭"))},i.onerror=l=>{console.error(l)},i.onmessage=l=>{Wo(l.data)},i.readyState==="open"&&fr(i)}function ot(){he.clear(),G&&$e.delete(G),B&&(B.onopen=null,B.onclose=null,B.onerror=null,B.onmessage=null,B.close(),B=null),L&&(L.onicecandidate=null,L.onconnectionstatechange=null,L.oniceconnectionstatechange=null,L.close(),L=null),G="",_e="p2p",ze=!1,be=!1,ue=!1,cs()}async function dr(i){if(!ur())throw new Error("当前浏览器不支持 WebRTC");if(await it(i,{initiate:!0}),(B==null?void 0:B.readyState)==="open")return B;const l=await $o(Bo(),of,"WebRTC 连接超时");if(!l||l.readyState!=="open")throw new Error("实时通道未建立");return l}function kn(i,l){if(!i||i.readyState!=="open")throw new Error("实时通道未就绪");i.send(JSON.stringify(l))}function Wo(i){try{const l=JSON.parse(String(i||"{}"));if(l.type==="text"){Vo(l);return}if(l.type==="file-meta"){qo(l);return}if(l.type==="file-chunk"){Go(l);return}l.type==="file-complete"&&Jo(l)}catch(l){console.error(l)}}function Vo(i){var T;const l=i.sender_device_id||G,u={id:l,name:i.sender_name||((T=Rt(l))==null?void 0:T.name)||`设备 ${lt(l)}`,type:jt(i.sender_type||"desktop"),connectionType:_e==="turn"?"TURN 中继已连接":"WebRTC 直连已连接"};M(u,!0);const h=p.value.find(E=>E.transferId===i.transfer_id);if(h&&h.kind==="text"){h.text=i.text||"",h.status="已接收",h.tone="success";return}p.value.push({id:Ue("incoming-text"),transferId:i.transfer_id,kind:"text",text:i.text||"",status:"已接收",tone:"success",copied:!1})}function qo(i){var T;const l=i.sender_device_id||G,u={id:l,name:i.sender_name||((T=Rt(l))==null?void 0:T.name)||`设备 ${lt(l)}`,type:jt(i.sender_type||"desktop"),connectionType:_e==="turn"?"TURN 中继已连接":"WebRTC 直连已连接"};M(u,!0),he.set(i.transfer_id,{name:rt(i.name,"file"),mimeType:i.mime_type||"application/octet-stream",sizeBytes:Number(i.size_bytes||0),receivedBytes:0,chunks:[]});let h=p.value.find(E=>E.transferId===i.transfer_id);h?(h.status="正在接收...",h.tone="primary",h.progress=0):(h={id:Ue("incoming-file"),transferId:i.transfer_id,kind:"file",name:rt(i.name,"file"),size:At(Number(i.size_bytes||0)),sizeBytes:Number(i.size_bytes||0),status:"正在接收...",tone:"primary",progress:0,pending:!1,downloadUrl:"",ownedDownloadUrl:!1},p.value.push(h))}function Go(i){const l=he.get(i.transfer_id);if(!l)return;const u=So(String(i.chunk_base64||""));l.receivedBytes+=Number(i.chunk_size||u.byteLength||0),l.chunks.push(u);const h=p.value.find(T=>T.transferId===i.transfer_id);if(h){const T=l.sizeBytes>0?l.receivedBytes/l.sizeBytes*100:0;h.progress=Math.max(1,Math.min(99,Math.round(T))),h.status="正在接收...",h.tone="primary"}}function Jo(i){const l=he.get(i.transfer_id);if(!l)return;const u=p.value.find(E=>E.transferId===i.transfer_id);if(!u){he.delete(i.transfer_id);return}const h=new Blob(l.chunks,{type:l.mimeType||"application/octet-stream"}),T=URL.createObjectURL(h);Bt(u,T,!0),u.progress=100,u.status="可保存",u.tone="success",he.delete(i.transfer_id)}function pr(i){return A.value.id.localeCompare(i)>0}function Mn(i,l="等待实时数据"){const u=Rt(i);return{id:i,deviceId:i,name:(u==null?void 0:u.name)||`设备 ${lt(i)}`,type:jt((u==null?void 0:u.type)||"desktop"),connectionType:l,network_group_key:(u==null?void 0:u.network_group_key)||""}}async function hr(i){const u=(i.payload||{}).description,h=i.device_id||"";if(!u||!h)return;M(Mn(h),!0);const T=await it(h);if(!T)return;const E=pr(h),se=!ze&&(T.signalingState==="stable"||ue),Et=u.type==="offer"&&!se;be=!E&&Et,!be&&(ue=u.type==="answer",await T.setRemoteDescription(u),ue=!1,u.type==="offer"&&(await T.setLocalDescription(),ce("webrtc.description",h,{description:T.localDescription})))}async function mr(i){const l=i.payload||{},u=i.device_id||"";if(!l.candidate||!u)return;(n.value!=="transfer"||m.value.deviceId!==u)&&M(Mn(u),!0);const h=await it(u);if(h)try{await h.addIceCandidate(l.candidate)}catch(T){be||console.error(T)}}async function us(){if(!(!L||L.connectionState!=="connected"))try{const i=await L.getStats();let l=null;if(i.forEach(E=>{E.type==="transport"&&E.selectedCandidatePairId&&(l=i.get(E.selectedCandidatePairId)||l)}),l||i.forEach(E=>{E.type==="candidate-pair"&&E.state==="succeeded"&&(E.nominated||E.selected)&&(l=E)}),!l)return;const u=i.get(l.localCandidateId),h=i.get(l.remoteCandidateId),T=(u==null?void 0:u.candidateType)==="relay"||(h==null?void 0:h.candidateType)==="relay";_e=T?"turn":"p2p",(B==null?void 0:B.readyState)==="open"&&We(T?"TURN 中继已连接":"WebRTC 直连已连接")}catch(i){console.error(i)}}function gr(){return _e==="turn"?"turn":"p2p"}function vr(){if(!A.value.id)return;const i=localStorage.getItem(Cs)||"";i&&(yr(),de=new WebSocket(`${_.value}?deviceId=${encodeURIComponent(A.value.id)}&deviceToken=${encodeURIComponent(i)}`),de.addEventListener("message",l=>{Xo(l.data)}),de.addEventListener("close",()=>{de=null,Yo()}),de.addEventListener("error",()=>{de==null||de.close()}))}function yr(){if(st&&(window.clearTimeout(st),st=null),!de)return;const i=de;de=null,i.onclose=null,i.close()}function Yo(){st||!A.value.id||(st=window.setTimeout(()=>{st=null,vr()},sf))}function ce(i,l,u){!de||de.readyState!==WebSocket.OPEN||!l||de.send(JSON.stringify({type:i,target_device_id:l,payload:u}))}function Xo(i){try{const l=JSON.parse(i);if(l.type==="presence.update"){v();return}if(l.type==="webrtc.description"){hr(l);return}if(l.type==="webrtc.candidate"){mr(l);return}if(l.type==="transfer.created"){_r(l);return}if(l.type==="transfer.updated"){br(l);return}if(l.type==="transfer.file"){wr(l);return}l.type==="peer.session.closed"&&Zo(l)}catch(l){console.error(l)}}function Zo(i){const l=i.device_id||"";!l||m.value.deviceId!==l||(ot(),D(),m.value={name:"--",type:"绛夊緟杩炴帴",baseType:"绛夊緟杩炴帴",deviceId:"",networkGroupKey:""},n.value="main")}function _r(i){var E;const l=i.payload||{},u=i.device_id||l.sender_device_id||"",h={id:u,name:l.sender_name||((E=Rt(u))==null?void 0:E.name)||`Device ${lt(u)}`,type:jt(l.sender_type||"desktop")};if(h.connectionType="等待实时数据",M(h,!0),!p.value.find(se=>se.transferId===l.transfer_id)){if(l.kind==="text"){p.value.push({id:Ue("incoming-text"),transferId:l.transfer_id,kind:"text",text:l.content||"",status:"已接收",tone:"success",copied:!1});return}p.value.push({id:Ue("incoming-file"),transferId:l.transfer_id,kind:"file",name:rt(l.name,"file"),size:At(Number(l.size_bytes||0)),sizeBytes:Number(l.size_bytes||0),status:"接收中...",tone:"primary",progress:35,pending:!1,downloadUrl:"",ownedDownloadUrl:!1})}}function br(i){const l=i.payload||{},u=p.value.find(h=>h.transferId===l.transfer_id);if(u&&u.kind==="file"){if(l.final_status==="completed"){u.progress=100,u.status="已接收",u.tone="success",u.downloadUrl&&(u.status="可保存");return}l.final_status==="cancelled"&&(u.status="已取消",u.tone="danger")}}function wr(i){const l=i.payload||{};let u=p.value.find(h=>h.transferId===l.transfer_id);!u&&l.transfer_id&&(u={id:Ue("incoming-file"),transferId:l.transfer_id,kind:"file",name:rt(l.name,"file"),size:"",sizeBytes:0,status:"可保存",tone:"success",progress:100,pending:!1,downloadUrl:"",ownedDownloadUrl:!1},p.value.push(u)),!(!u||u.kind!=="file")&&(Bt(u,l.download_url||l.data_url||"",!1),u.status="可保存",u.progress=100,u.tone="success")}function Rt(i){return r.value.find(l=>l.id===i)}function xr(i,l={}){return[{label:"在线设备",value:`${i.devices_online||0}`,tone:"blue"},{label:"待加入房间",value:`${i.rooms_waiting||0}`,tone:"cyan"},{label:"有效传输",value:`${i.transfers_total||0}`,tone:"default"},{label:"累计传输",value:`${i.transfers_cumulative||0}`,tone:"default"},{kind:"minio",label:"MinIO 剩余容量",value:fs(l.remaining_bytes||0),tone:Number(l.usage_percent||0)>=85?"danger":Number(l.usage_percent||0)>=60?"cyan":"blue",percent:Math.max(0,100-Number(l.usage_percent||0)),detail:`已用 ${fs(l.used_bytes||0)} / 总计 ${fs(l.capacity_bytes||0)}`,kicker:`存档 ${l.object_count||0} 份`}]}function fs(i){const l=Number(i||0);if(!l||l<=0)return"0 GB";const u=["B","KB","MB","GB","TB"],h=Math.min(Math.floor(Math.log(l)/Math.log(1024)),u.length-1),T=l/1024**h,E=h>=3?2:T>=10?1:2;return`${T.toFixed(E)} ${u[h]}`}function Ir(i){const l=i.final_status==="completed",u=i.final_status==="failed"||i.final_status==="cancelled";return{time:Rn(i.created_at),peer:`${lt(i.sender_device_id)} -> ${lt(i.receiver_device_id)}`,type:i.kind==="text"?"文本消息":`文件 ${i.name}`,size:At(Number(i.size_bytes||0)),status:l?`已完成 (${i.current_channel||"p2p"})`:u?`已结束 (${i.final_status})`:`进行中 (${i.final_status||"pending"})`,tone:l?"success":u?"danger":"primary"}}function lt(i){return i?i.slice(0,8):"--"}function Rn(i){if(!i)return"刚刚";const l=new Date(i),u=Date.now()-l.getTime();if(!Number.isFinite(u))return"刚刚";const h=Math.max(0,Math.floor(u/1e3));if(h<60)return`${h} 秒前`;const T=Math.floor(h/60);if(T<60)return`${T} 分钟前`;const E=Math.floor(T/60);return E<24?`${E} 小时前`:`${Math.floor(E/24)} 天前`}function Ue(i){return`${i}-${Date.now()}-${Math.random().toString(36).slice(2,8)}`}function At(i){if(!i||i<=0)return"0 B";const l=["B","KB","MB","GB","TB"],u=Math.min(Math.floor(Math.log(i)/Math.log(1024)),l.length-1),h=i/1024**u,T=h>=10||u===0?0:1;return`${h.toFixed(T)} ${l[u]}`}v=async function(){return A.value.id?Fn.listCandidates(A.value.id).then(l=>{const u=Array.isArray(l)?l:[];r.value=u.map(h=>({...h,description:`${jt(h.type)} · 最近活跃 ${Rn(h.last_seen_at)}`,icon:No(h.type),connectionType:ar(h.network_group_key)?"局域网直连优先":"跨网络实时传输"})),s.value=r.value.length===0}).catch(l=>{s.value=!1,console.error(l)}):Promise.resolve()},P=function(l){const u=l.deviceId||l.id||"",h=l.connectionType||l.type||"点对点传输";b(),m.value.deviceId!==u&&(ot(),D()),m.value={name:l.name,type:h,baseType:h,deviceId:u,networkGroupKey:l.network_group_key||""},a.value=!1,c.value="----",n.value="transfer",We("正在建立实时通道"),it(u,{initiate:!0})},M=function(l,u=!1){const h=l.deviceId||l.id||"",T=l.connectionType||l.type||"点对点传输";m.value.deviceId===h&&n.value==="transfer"||(ot(),u||D()),m.value={name:l.name,type:T,baseType:T,deviceId:h,networkGroupKey:l.network_group_key||""},a.value=!1,c.value="----",n.value="transfer",h&&(We("正在建立实时通道"),it(h))},O=function(){m.value.deviceId&&ce("peer.session.closed",m.value.deviceId,{}),ot(),D(),m.value={name:"--",type:"等待连接",baseType:"等待连接",deviceId:"",networkGroupKey:""},n.value="main"},W=async function(l){const u=l.trim();if(u){if(!m.value.deviceId){window.alert("当前没有可用的接收端");return}try{const h=await ge.create({kind:"text",name:"text-message",content:u,sender_device_id:A.value.id,receiver_device_id:m.value.deviceId});try{await rn(h,u)}catch(T){console.warn("realtime text send failed, fallback to relay",T),await rr(h,u)}p.value.push({id:Ue("text"),transferId:h.id,kind:"text",text:u,status:"已发送",tone:"success",copied:!1})}catch(h){window.alert(`发送文本失败:${h.message}`)}}};function Sr(i,l){ce("transfer.file",m.value.deviceId,{transfer_id:i.transferId,name:i.name,download_url:l.download_path||l.download_url})}function Cr(i,l,{onProgress:u}={}){if(!(i!=null&&i.file))return Promise.reject(new Error("未找到待上传文件"));if(!(l!=null&&l.fallback_allowed))return Promise.reject(new Error("MinIO 存档未启用"));const h=l.id;if(Te.has(h))return Te.get(h);const T=(async()=>(await ge.presignFallback(h),ge.uploadFallback(h,i.file,E=>{typeof u=="function"&&u(E)})))().finally(()=>{Te.delete(h)});return Te.set(h,T),T}async function Tr(i,l,u){await ge.updateStatus(l.id,{current_channel:"minio",final_status:"completed"}),ce("transfer.updated",m.value.deviceId,{transfer_id:l.id,final_status:"completed",current_channel:"minio"}),Sr(i,u),i.progress=100,i.status="已上传到 MinIO,对方可直接领取",i.tone="success"}async function Qo(i,l){const u=$e.get(i);if(!(!(u!=null&&u.length)||!(l!=null&&l.remoteDescription))){$e.delete(i);for(const h of u)try{await l.addIceCandidate(h)}catch(T){console.error(T)}}}return me=async function(l,u){l.progress=Math.max(5,l.progress||0),l.status="正在切换到 MinIO...",l.tone="primary";try{ce("transfer.updated",m.value.deviceId,{transfer_id:l.transferId,final_status:"fallback_uploading",current_channel:"minio"});const h=await Cr(l,u,{onProgress:T=>{l.progress=Math.max(5,Math.min(T,99))}});await Tr(l,u,h)}catch(h){l.pending=!0,l.status=`上传失败:${h.message}`,l.tone="danger"}},hr=async function(l){const h=(l.payload||{}).description,T=l.device_id||"";if(!h||!T)return;M(Mn(T),!0);const E=await it(T);if(!E)return;const se=pr(T),Et=!ze&&(E.signalingState==="stable"||ue),el=h.type==="offer"&&!Et;if(be=!se&&el,!be&&!(h.type==="answer"&&(E.signalingState!=="have-local-offer"||!E.localDescription))){try{ue=h.type==="answer",await E.setRemoteDescription(h),await Qo(T,E)}catch(ds){console.error(ds)}finally{ue=!1}if(h.type==="offer")try{await E.setLocalDescription(),ce("webrtc.description",T,{description:E.localDescription})}catch(ds){console.error(ds)}}},mr=async function(l){const u=l.payload||{},h=l.device_id||"",T=u.candidate;if(!T||!h)return;(n.value!=="transfer"||m.value.deviceId!==h)&&M(Mn(h),!0);const E=await it(h);if(E){if(!E.remoteDescription){const se=$e.get(h)||[];se.push(T),$e.set(h,se);return}try{await E.addIceCandidate(T)}catch(se){be||console.error(se)}}},J=async function(l){const u=p.value.find(h=>h.id===l);if(!(!u||u.kind!=="file"||!u.pending)){if(!m.value.deviceId){window.alert("当前没有可用的接收端");return}u.pending=!1,u.status="创建传输中...",u.tone="primary";try{const h=await ge.create({kind:"file",name:u.name,size_bytes:u.sizeBytes,sender_device_id:A.value.id,receiver_device_id:m.value.deviceId});u.transferId=h.id;const T=h.fallback_allowed?Cr(u,h).catch(E=>{throw console.warn("minio backup sync failed",E),E}):Promise.resolve(null);ce("transfer.created",m.value.deviceId,{transfer_id:h.id,kind:"file",name:u.name,size_bytes:u.sizeBytes,sender_device_id:A.value.id,sender_name:A.value.name,sender_type:A.value.type,receiver_device_id:m.value.deviceId,final_status:"connecting",current_channel:"p2p",transport_options:as()});try{if(await ir(u,h),h.fallback_allowed){u.status="实时传输完成,正在同步云端备份...",u.tone="primary";try{const E=await T;E&&(Sr(u,E),u.status="已发送,2 小时内可离线领取")}catch(E){u.status=`实时传输成功,但 MinIO 备份失败:${E.message}`,u.tone="danger";return}u.tone="success"}}catch(E){console.warn("realtime file send failed, fallback to minio",E);try{const se=await T;if(se){await Tr(u,h,se);return}}catch(se){console.warn("minio backup sync failed after realtime failure",se)}await or(u,h)}}catch(h){u.pending=!0,u.status=`发送失败:${h.message}`,u.tone="danger"}}},_r=function(l){var se;const u=l.payload||{},h=l.device_id||u.sender_device_id||"",T={id:h,name:u.sender_name||((se=Rt(h))==null?void 0:se.name)||`设备 ${lt(h)}`,type:jt(u.sender_type||"desktop"),connectionType:"等待实时数据"};if(M(T,!0),!p.value.find(Et=>Et.transferId===u.transfer_id)){if(u.kind==="text"){u.content&&p.value.push({id:Ue("incoming-text"),transferId:u.transfer_id,kind:"text",text:u.content||"",status:"已接收",tone:"success",copied:!1});return}p.value.push({id:Ue("incoming-file"),transferId:u.transfer_id,kind:"file",name:rt(u.name,"file"),size:At(Number(u.size_bytes||0)),sizeBytes:Number(u.size_bytes||0),status:"等待接收...",tone:"primary",progress:5,pending:!1,downloadUrl:"",ownedDownloadUrl:!1})}},br=function(l){const u=l.payload||{},h=p.value.find(T=>T.transferId===u.transfer_id);if(h&&h.kind==="file"){if(u.final_status==="completed"){h.progress=100,h.status=h.downloadUrl?"可保存":"传输完成",h.tone="success";return}if(u.final_status==="cancelled"){h.status="已取消",h.tone="danger";return}u.final_status==="fallback_uploading"&&(h.status="发送端正在上传回退文件...",h.tone="primary")}},wr=function(l){const u=l.payload||{};let h=p.value.find(T=>T.transferId===u.transfer_id);!h&&u.transfer_id&&(h={id:Ue("incoming-file"),transferId:u.transfer_id,kind:"file",name:rt(u.name,"file"),size:"",sizeBytes:0,status:"可保存",tone:"success",progress:100,pending:!1,downloadUrl:"",ownedDownloadUrl:!1},p.value.push(h)),!(!h||h.kind!=="file")&&(Bt(h,u.download_url||u.data_url||"",!1),h.status="可保存",h.progress=100,h.tone="success")},Ir=function(l){const u=l.final_status==="completed",h=l.final_status==="failed"||l.final_status==="cancelled",T=rt(l.name,"file");return{time:Rn(l.created_at),peer:`${lt(l.sender_device_id)} -> ${lt(l.receiver_device_id)}`,type:l.kind==="text"?"文本消息":`文件 ${T}`,size:At(Number(l.size_bytes||0)),status:u?`已完成(${l.current_channel||"p2p"})`:h?`已结束(${l.final_status})`:`进行中(${l.final_status||"pending"})`,tone:u?"success":h?"danger":"primary"}},(i,l)=>(j(),V("div",null,[y("div",Zu,[q(Oc,{theme:t.value,onToggleTheme:Cn},null,8,["theme"]),n.value==="main"?(j(),V("div",Qu,[q(gu,{devices:r.value,"is-scanning":s.value,"local-device-name":A.value.name,onSelectDevice:P},null,8,["devices","is-scanning","local-device-name"]),q(ku,{"generated-code":c.value,"is-waiting":a.value,"pending-downloads":f.value,"room-code-input":o.value,onCancelRoom:R,onCreateRoom:x,onJoinRoom:k,onUpdateRoomCode:d},null,8,["generated-code","is-waiting","pending-downloads","room-code-input"])])):Be("",!0),n.value==="transfer"?(j(),vt(ju,{key:1,"connection-type":m.value.type,"has-pending-items":$t.value,items:p.value,"network-hint":Lt.value,"peer-name":m.value.name,onClose:O,onCopyItem:Fe,onFilesSelected:ee,onRemoveItem:Ne,onSendAllPending:we,onSendText:W,onStartUpload:J},null,8,["connection-type","has-pending-items","items","network-hint","peer-name"])):Be("",!0),n.value==="admin"?(j(),vt(ou,{key:2,"file-limit":$.value,"minio-capacity":S.value,records:N.value,stats:U.value,onExit:Mo,onSaveConfig:Ro,"onUpdate:fileLimit":l[0]||(l[0]=u=>$.value=u),"onUpdate:minioCapacity":l[1]||(l[1]=u=>S.value=u)},null,8,["file-limit","minio-capacity","records","stats"])):Be("",!0)]),q(Rc,{onRequestAdmin:ko})]))}};Tc(af).mount("#app");
diff --git a/frontend/dist/index.html b/frontend/dist/index.html
index b945419..230d685 100644
--- a/frontend/dist/index.html
+++ b/frontend/dist/index.html
@@ -5,7 +5,7 @@
AirShare Pro
-
+
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 29f504d..0ae017d 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -49,6 +49,7 @@ const localDevice = ref({
id: '',
name: '',
type: '',
+ networkGroupKey: '',
})
const persistedDeviceId = localStorage.getItem(DEVICE_ID_KEY) || ''
@@ -233,6 +234,7 @@ async function registerCurrentDevice() {
id: device.id,
name: device.name,
type: device.type,
+ networkGroupKey: device.network_group_key || '',
}
await loadPendingDownloads()
@@ -1284,7 +1286,7 @@ function deriveNetworkGroupKey() {
}
function isSameLocalNetwork(networkGroupKey) {
- const localNetworkGroupKey = deriveNetworkGroupKey()
+ const localNetworkGroupKey = localDevice.value.networkGroupKey || deriveNetworkGroupKey()
return !!localNetworkGroupKey && networkGroupKey === localNetworkGroupKey
}