const{createApp}=Vue const{createVuetify,useGoTo,useDisplay}=Vuetify var data={alert:{show:false,color:'success',text:'',timeout:0,},theme:{dark:false,},nav:{showDrawer:false,showTOC:true,tocPanel:0,tab:'account',post:{discussionId:6785,currentPage:1,targetPage:1,total:31,anchor:0,goToOptions:{container:null,duration:0,easing:'easeInOutCubic',offset:-100,},worker:null,task:[],active:[],apiLock:[],originLike:new Map([]),},related:{block:1,}},search:{width:80,text:null,loading:false,},tags:[{id:15,url:'/t/0f074b236a045c7665770f3a1528',name:'软路由',color:'#66BB6A',icon:'mdi-router-wireless-settings',},],posts:[{id:83340,num:0,uid:2611,content:'\u003Cp\u003E如题,非软文,放心看。\u003C/p\u003E\u003Cp\u003E我之前用的是 ZeroTier ,参见这帖: https://www.v2ex.com/t/1113181 。遇到了两个问题:\u003C/p\u003E\u003Cp\u003E连接过程极不透明。除了名称和延迟,几乎没办法得知和连接现状有关的任何信息(走哪个中转?路由表如何?上次连接错误、重试连接是几分钟前?现在处于断开、尝试打洞中还是已连接?)。\u003C/p\u003E\u003Cp\u003E七八年前就有人在官网论坛提需求,开发者表示根本不 care 普通用户:有检查网络的需求,自己编译 debug 版看日志去。但事实是我的网络不是很稳定(参见上面的帖子),所以经常莫名其妙断开,也没法排查原因。\u003C/p\u003E\u003Cp\u003EVPN 节点不对等,不能动态路由。 ZeroTier (以及 Tailscale 、n3n 等)都抽象出了多种节点类型:普通客户端、中转服务器( Moon/DERP/Supernode )、控制服务器( Planet/Headscale ),严格来说不算 Full Mesh VPN ,只能说是支持打洞的中心化 P2P VPN 。\u003C/p\u003E\u003Cp\u003E这导致一个什么问题呢?只有 Moon 服务器(和手动配路由表的机器)才可以中转流量。然而我网络情况经常变:明明「家里的 A \u0026lt;-\u0026gt; 我的电脑 \u0026lt;-\u0026gt; 单位的 B 」,但因为 A 和 B 无法直连、我的电脑又不是 Moon ,结果 A \u0026lt;-\u0026gt; B 通信只能走较慢的公网中转。\u003C/p\u003E\u003Cp\u003E这个问题其实挺好解决的,最简单的思路就运行一个 OSPF/BGP/RIP 客户端来动态路由嘛。V2EX 早就有朋友尝试过了 [1],也有人提了 issues [2,3],但无一例外被忽略了,不清楚什么理由。\u003C/p\u003E\u003Cp\u003EEasyTier 基本就解决了这两个痛点。第一点,它有一个信息非常细的 cli 工具和 WebUI,路由表、延迟、下一跳中转、上次通信时间、通信流量都可以看,也有系统日志;第二点,它是完全的去中心化 VPN,所有网络内的节点(包括官方的公益服务器)全部都是对等端,执行一样的功能,既能互相访问又能负责中转。它运行了一个 OSPF 来零配置动态路由。\u003C/p\u003E\u003Cp\u003E此外,它的配置实在太无脑了,就「下载程序、运行」两步,文档也写得很好;相比上面这些,它还支持多协议通信( TCP/UDP/KCP )。总之,没有遇到任何大问题。ZeroTier 的断连现象也没有观察到了。\u003C/p\u003E\u003Cp\u003E缺点有三个:\u003C/p\u003E\u003Cp\u003E第一,它和 tailscale 一样是基于 Wireguard ,所以不是 L2 VPN ,不支持组播和广播(相对地,它支持用 Wireguard 客户端直接连接)。不过,它出了个游戏联机启动器,通过 WinIPBroadcast 来自动转发广播流量,联机游戏应该是没什么问题。\u003C/p\u003E\u003Cp\u003E第二,它是 Rust 开发,目前移动端只支持 Android 不支持 iOS ;\u003C/p\u003E\u003Cp\u003E第三,完全去中心化的网络,没有传统意义上的「控制节点」,管理起来肯定会有些困难。\u003C/p\u003E\u003Cp\u003E以防广告嫌疑,不放 EasyTier 链接了。我目前是和 ZeroTier 共用,主用 EasyTier ,互为替补。\u003C/p\u003E\u003Cp\u003E[1] https://www.v2ex.com/t/707060\u003C/p\u003E\u003Cp\u003E[2] https://github.com/zerotier/ZeroTierOne/issues/798\u003C/p\u003E\u003Cp\u003E[3] https://github.com/tailscale/tailscale/issues/11383\u003C/p\u003E',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 12:01:10',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83341,num:1,uid:2340,content:'我也苦恼 Zerotier 这些沉疴已久,感谢楼主指路,我也去探索一番 EasyTier',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 13:03:03',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83342,num:2,uid:417,content:'请问这种的纯中转速度怎么样?',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 13:46:59',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83343,num:3,uid:5116,content:'可能最大的优势就是简单吧.\u003Cbr\u003Ebtw. op 为什么需要 full mesh ?全互联并不是一种优势.',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 13:56:00',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83344,num:4,uid:3617,content:'和 tailscale 比呢',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 14:23:47',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83345,num:5,uid:5031,content:'tailscale 已经很好用了,没有到需要换的时候,长期远程连到家里的服务器做开发,完全感觉不到有问题',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 14:37:53',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83346,num:6,uid:6041,content:'如果基于 Wireguard 的话,iOS 的 Wireguard 客户端可以连上吗',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 14:41:11',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83347,num:7,uid:2611,content:'底层用的是 wg ,性能应该不差,我测试都可以跑满上行。\u003Cbr\u003E\u003Cbr\u003ETailscale 我其实没实际用过,不敢乱发言了。从我读到的文档来看,用户体验( UI 、线上管理)可能稍差,但功能性上肯定是更强的。\u003Cbr\u003E\u003Cbr\u003E我的场景需要 full mesh 呀。比如上面提到的 A \u0026lt;-\u0026gt; B \u0026lt;-\u0026gt; C 下,AC 直连的问题。\u003Cbr\u003E\u003Cbr\u003E文档里说是可以的,但可能享受不到其他好处( OSPF/自动 DHCP )了。',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 15:14:35',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83348,num:8,uid:2680,content:'之前 2 个缺点,新软件 3 个缺点,一起就 5 个缺点了',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 15:27:51',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83349,num:9,uid:417,content:'这么牛逼能跑满?话说你确定你走的是中转而不是直连吗?我的 tailscale 走的自建中转慢得要死,顶多 15Mbps',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 15:58:43',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83350,num:10,uid:5806,content:'好像 EasyTier 的私有中转服务器鉴权功能还没有,容易被扫被偷跑,不知道什么时候会加上',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 16:03:45',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83351,num:11,uid:5987,content:'他们的文档写得很烂啊,一点都不友好',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 16:10:46',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83352,num:12,uid:241,content:'+1, 替换 ZeroTier 绰绰有余\u003Cbr\u003E唯一的遗憾是用户态 Wireguard\u003Cbr\u003E心里最想要的其实是内核态 Wireguard + 充分利用 p2p 的自动配置器',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 16:16:00',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83353,num:13,uid:684,content:'感谢楼主\u003Cbr\u003Etailscale 在国内实践起来真是一坨 --- 买国内 vps 搭建 derp 被封锁外网, 用自己的公网 IP 群辉搭 derp 客户端太老一直报错\u003Cbr\u003E\u003Cbr\u003E每次公司和住所连接都是 300ms 的 ping.\u003Cbr\u003E\u003Cbr\u003E用这个加上公网 vps 终于不用看一堆 derp 搭建文章也会通过 公网 vps 的客户端来中继了',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 16:28:27',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[17,],mentionUsers:[],likeUsers:[],},{id:83354,num:14,uid:15147,content:'用过,确实可以的,界面有点丑,但是信息给的很全面,是优点\u003Cbr\u003E\u003Cbr\u003E.cn 域名的不敢用,又不是微信这种,能自主选择的都不想用',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 17:28:16',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83355,num:15,uid:15148,content:'Register Failed\u003Cbr\u003EFailed to register, error: {\u0026#34;message\u0026#34;:\u0026#34;Execution Error: error returned from database: (code: 2067) UNIQUE constraint failed: users.username\\n\\nCaused by:\\n 0: error returned from database: (code: 2067) UNIQUE constraint failed: users.username\\n 1: error returned from database: (code: 2067) UNIQUE constraint failed: users.username\\n 2: (code: 2067) UNIQUE constraint failed: users.username\u0026#34;}\u003Cbr\u003E\u003Cbr\u003E官网注册报错这个,厉害了!',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 18:01:04',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83356,num:16,uid:1226,content:'ts 还是更 nice 的',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 20:04:13',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83357,num:17,uid:4961,content:'#13 \u003Cbr\u003E\u003Cbr\u003E\u0026#34;买国内 vps 搭建 derp 被封锁外网\u0026#34; 不会吧?\u003Cbr\u003E\u003Cbr\u003E我现在就是用的阿里云纯 IP 搭 DERP 啊,没什么问题啊.\u003Cbr\u003E\u003Cbr\u003E或者说你是国内国外组网不是纯国内组网?',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 20:17:24',updatedAt:'2025-03-29 12:04:39',mentionNum:13,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83358,num:18,uid:15149,content:'是的,我也是国内阿里搭建的 derp ,非常爽',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 21:07:26',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[],mentionUsers:[],likeUsers:[],},{id:83359,num:19,uid:684,content:'我连 8.8.8.8 都 ping 不通了,每次提工单换一个服务器,要不了几个月就会这样,然后就是连不上 tailscale 本身的服务器,自然 derp 也就无法工作. 纯国内组网',ipRegion:'',updatedByUid:0,createdAt:'2025-03-26 21:43:09',updatedAt:'2025-03-29 12:04:39',mentionNum:0,mentionedBy:[23,],mentionUsers:[],likeUsers:[],},],usersMap:new Map([[2611,{uid:2611,url:'/u/3b3a40016a045c75666b576f4125121d',avatar:'/a/3b3a40016a045c75666b576f4125121d',username:'w568w🤖',}],[15148,{uid:15148,url:'/u/032f5a3b6a045d72616e5e6f035a632b',avatar:'/a/032f5a3b6a045d72616e5e6f035a632b',username:'lmaq🤖',}],[3617,{uid:3617,url:'/u/1b2358356a045c74666b516f152f3d05',avatar:'/a/1b2358356a045c74666b516f152f3d05',username:'Tink🤖',}],[2340,{uid:2340,url:'/u/387b7e2f6a045c75636e566f125d182e',avatar:'/a/387b7e2f6a045c75636e566f125d182e',username:'aa51513🤖',}],[6041,{uid:6041,url:'/u/1b3f770b6a045c71606e576f315c2477',avatar:'/a/1b3f770b6a045c71606e576f315c2477',username:'pxiphx891🤖',}],[241,{uid:241,url:'/u/16034b1b6a045c77626e576f45222332',avatar:'/a/16034b1b6a045c77626e576f45222332',username:'1423🤖',}],[15147,{uid:15147,url:'/u/6e0c4a5a6a045d72616e516f3808292a',avatar:'/a/6e0c4a5a6a045d72616e516f3808292a',username:'la0wei🤖',}],[5987,{uid:5987,url:'/u/171a443a6a045c726962516f231c3972',avatar:'/a/171a443a6a045c726962516f231c3972',username:'simplove🤖',}],[4961,{uid:4961,url:'/u/093b02016a045c73696c576f40280422',avatar:'/a/093b02016a045c73696c576f40280422',username:'HFX3389🤖',}],[1226,{uid:1226,url:'/u/3e1a712e6a045c766268506f1d51633e',avatar:'/a/3e1a712e6a045c766268506f1d51633e',username:'SenLief🤖',}],[417,{uid:417,url:'/u/02256b226a045c77646b516f341e3e20',avatar:'/a/02256b226a045c77646b516f341e3e20',username:'liuzimin🤖',}],[5031,{uid:5031,url:'/u/1e1c4b3e6a045c726069576f3e001832',avatar:'/a/1e1c4b3e6a045c726069576f3e001832',username:'sarices🤖',}],[2680,{uid:2680,url:'/u/1f235f586a045c756662566f0733192f',avatar:'/a/1f235f586a045c756662566f0733192f',username:'iceecream🤖',}],[5806,{uid:5806,url:'/u/362058266a045c72686a506f0e083b12',avatar:'/a/362058266a045c72686a506f0e083b12',username:'lany🤖',}],[684,{uid:684,url:'/u/232c68556a045c776662526f471d1213',avatar:'/a/232c68556a045c776662526f471d1213',username:'m1nm13🤖',}],[15149,{uid:15149,url:'/u/2c180b2f6a045d72616e5f6f3c111d1d',avatar:'/a/2c180b2f6a045d72616e5f6f3c111d1d',username:'jip🤖',}],[2716,{uid:2716,url:'/u/147906386a045c75676b506f163e0321',avatar:'/a/147906386a045c75676b506f163e0321',username:'lnbiuc🤖',}],[5116,{uid:5116,url:'/u/223d5d2f6a045c72616b506f1c3b1e20',avatar:'/a/223d5d2f6a045c72616b506f1c3b1e20',username:'Int100🤖',}],]),related:[{title:'tailscale 和 zerotier 哪个好用,功能多?',url:'/d/360b75096a045c77606a567a445d6a6a2b280405',},{title:'Openwrt 上如何设置递归静态路由',url:'/d/2f2c575e6a045c77606a567a465f676a5b2f3139',},{title:'小米 wifi7 路由器千兆都跑不满',url:'/d/313003376a045c77606a567a475c606a1c101014',},{title:'家庭路由器组网求助',url:'/d/3711025f6a045c77606a56754f5e636a3b0c3505',},{title:'目前好像感受到了 openwrt 作为主路由的无力感',url:'/d/373048036a045c77606a56744051616a2517642b',},{title:'路由器选择就推荐,小米还是中兴呢?或者华硕呢',url:'/d/6a3e555a6a045c77606a5674405f676a282f0920',},{title:'畅网 N100 软路由死机频繁, PVE + 爱快 + IstoreOS 配置,换硬盘后仍未解决,求助!',url:'/d/037d540e6a045c77606a56744251656a38131d3e',},{title:'城中村租房 OpenWrt 抢共享带宽问题求助',url:'/d/6b3b512b6a045c77606a5674425d626a2e731038',},{title:'两个路由器之间的电力猫应该如何配置?',url:'/d/0f784b076a045c77606a56744258656a2b711f64',},{title:'大佬们能帮我看看拓扑图吗?现在旁路无法上网,折腾两天了,头都大了。',url:'/d/3f2c713a6a045c77606a56744358656a15356b34',},{title:'求推荐带两路 SFP+口的路由器',url:'/d/620859076a045c77606a5674455f656a1b131f15',},{title:'你们的AdGuard home能去youtube客户端广告吗',url:'/d/1c7c482c6a045c77606a5674465f6a6a5b2f0319',},{title:'有什么好用的软路由系统推荐吗',url:'/d/3d3b7e1d6a045c77606a5674465b636a2902231f',},{title:'openwrt 去广告的方案',url:'/d/1f3c621a6a045c77606a56774550656a0d326304',},{title:'r2s 的内存卡(带op系统 ) 插r4s 上能否直接使用。',url:'/d/2f38780f6a045c77606a5677455b6b6a0a7a1a2a',},{title:'AdGuard现在哪里还有活动吗?',url:'/d/1d1077186a045c77606a56764e5b616a162f6736',},{title:'r2s 只能跑满300兆啊。4s 5s 哪个能跑满千啊',url:'/d/080b7a2b6a045c77606a5676415c6a6a1c20270b',},{title:'分享一下用AdguardHome和Nginx的配置,让你可以自由DIY路径',url:'/d/6d10533f6a045c77606a5676415a626a0f273467',},{title:'adguard home的doh服务终于搭建成功了',url:'/d/69707b0c6a045c77606a567641586b6a5d361b64',},{title:'AdGuard home过滤手机广告——服务端',url:'/d/2a250a046a045c77606a5671405e636a0a3a2126',},],} const App={setup(){const goTo=useGoTo() const{mdAndUp}=useDisplay() return{goTo,mdAndUp}},data(){return data;},mounted(){const themeDark=localStorage.getItem("themeDark") if(themeDark!==null){this.theme.dark=JSON.parse(themeDark)} if(this.nav.post.total>(this.nav.post.currentPage-1)*100+20){let moreLen=100 if(this.nav.post.total({id:null,num:(this.nav.post.currentPage-1)*100+v,uid:null,content:null,ipRegion:null,updatedByUid:null,createdAt:null,updatedAt:null,mentionNum:null,mentionedBy:null,mentionUsers:null,likeUsers:null,})) this.posts.push(...morePosts.slice(20))} this.workerStart() const hash=window.location.hash const match=hash.match(/#(\d+)/) if(match){const n=parseInt(match[1],10) if(n>=(this.nav.post.currentPage-1)*100&&n{this.jumpTo(n)})}} this.$nextTick(()=>{this.addHeadingIds() tocbot.init({tocSelector:'.toc',contentSelector:'#post-content-0',headingSelector:'h2, h3, h4',headingsOffset:100,scrollSmoothOffset:-100,scrollSmooth:true,collapseDepth:6,onClick:function(e){setTimeout(()=>{history.replaceState(null,'',window.location.pathname+window.location.search)},0)},}) tocbot.refresh()});},beforeUnmount(){this.workerStop() if(this.quill){this.quill.destroy() this.quill=null}},computed:{dposts(){return this.posts.slice(20);},},created(){},methods:{successAlert(msg){this.alert={show:true,color:'success',text:msg,timeout:1500,}},failureAlert(msg){this.alert={show:true,color:'error',text:msg,timeout:5000,}},flipThemeDark(){this.theme.dark=!this.theme.dark localStorage.setItem("themeDark",JSON.stringify(this.theme.dark))},toSearch(){if(!this.search.text){this.failureAlert('搜索词不能为空') return} let keywords=this.search.text.trim() if(keywords.length<1){this.failureAlert('搜索词不能为空') return} if(keywords.length>100){this.failureAlert('搜索词过长') return} this.doSearch(keywords)},toReg(){window.location.href="/reg"},toLogin(){window.location.href="/login"},toPage(){let url=window.location.href url=url.replace(/(\/\d+)?(#[0-9]+)?$/,this.nav.post.targetPage>1?`/${this.nav.post.targetPage}`:'') window.location.href=url},toLoadRelated({done}){if(this.my&&this.my.uid){this.apiLoadRelated({done})}else{done('ok')}},workerStart(){this.nav.post.worker=setInterval(()=>{this.workerLoad()},500);},workerStop(){if(this.nav.post.worker){clearInterval(this.nav.post.worker);this.nav.post.worker=null;}},async jumpTo(num){const page=Math.floor(num/100)+1 const i=num-(page-1)*100 if(page===this.nav.post.currentPage){this.goTo("#post-"+num,this.nav.post.goToOptions) if(!this.posts[i].id){const block=Math.floor(num/20)+1 this.nav.post.apiLock[block]=true await this.apiLoadPosts(block) this.$nextTick(()=>{this.goTo("#post-"+num,this.nav.post.goToOptions)})}}else{let url=window.location.href url=url.replace(/(\/\d+)?(#[0-9]+)?$/,page>1?`/${page}`:'') url=url+"#"+num window.location.href=url}},postIntersect(num){return(isIntersecting,entries,observer)=>{if(isIntersecting){this.nav.post.task.push(num) this.nav.post.active.push(num) this.nav.post.active=this.nav.post.active.filter(item=>Math.abs(item-num)<=5) this.nav.post.active.sort((a,b)=>a-b)}else{this.nav.post.active=this.nav.post.active.filter(item=>item!==num)} if(this.nav.post.active[0]){this.nav.post.anchor=this.nav.post.active[0]}else{this.nav.post.anchor=0}}},async apiLoadPosts(block){try{const response=await axios.post('/fapi/v1/post/block/'+block,{discussionId:this.nav.post.discussionId,}) if(response.data.code===0){response.data.data.posts.forEach(post=>{const i=post.num%100 Object.assign(this.posts[i],post)}) response.data.data.users.forEach(user=>{this.usersMap.set(user.uid,user)})}else{this.failureAlert('回帖数据加载失败: '+response.data.msg)}}catch(error){this.failureAlert('回帖数据加载失败: '+error)} this.nav.post.apiLock[block]=false},workerLoad(){while(this.nav.post.task.length){const num=this.nav.post.task.pop() const i=num-(this.nav.post.currentPage-1)*100 if(!this.posts[i].id){const block=Math.floor(num/20)+1 if(!this.nav.post.apiLock[block]){this.nav.post.apiLock[block]=true this.apiLoadPosts(block)}}}},getTimeInfo(t){if(!t){return ""} const now=new Date();const then=new Date(t);const diff=now-then;const minute=60*1000;const hour=minute*60;const day=hour*24;const month=day*30;const year=month*12;if(diffpost.num===num) if(!post){return "#"+num} const uid=post.uid const username=this.usersMap.get(uid)?.username if(!username){return "#"+num} return username},getUsernameByPostId(id){const post=this.posts.find(post=>post.id===id) if(!post){return "#"+this.getPostNumByPostId(id)} const uid=post.uid const username=this.usersMap.get(uid).username if(!username){return "#"+this.getPostNumByPostId(id)} return username},getPostNumByPostId(id){const post=this.posts.find(post=>post.id===id) return post.num},getPostById(id){const post=this.posts.find(post=>post.id===id) return post},getPostByNum(num){const post=this.posts.find(post=>post.num===num) return post},getAvatarByUid(uid){const avatar=this.usersMap.get(uid)?.avatar if(!avatar){return this.getRandomAvatar()} return avatar},getAvatarByPostNum(num){const post=this.posts.find(post=>post.num===num) if(!post){return this.getRandomAvatar()} const uid=post.uid return this.getAvatarByUid(uid)},getRandomAvatar(){const num=Math.floor(Math.random()*100) return "https://randomuser.me/api/portraits/men/"+num+".jpg"},getUrlByUid(uid){const url=this.usersMap.get(uid)?.url if(!url){return ""} return url},getTextByPostNum(num){const post=this.posts.find(post=>post.num===num) if(!post||!post.content){return '点击跳转到#'+num+'查看'} const parser=new DOMParser() const doc=parser.parseFromString(post.content,'text/html') const text=doc.body.textContent||'' return text.slice(0,100)},addHeadingIds(){const content=document.getElementById('post-content-0') if(!content){this.nav.showTOC=false return} const headings=content.querySelectorAll('h2, h3, h4') headings.forEach((heading,index)=>{if(!heading.id){heading.id=`toc-nav-${index}`}}) if(headings.length==0){this.nav.showTOC=false}},async doSearch(keywords){this.search.loading=true try{const response=await axios.post('/fapi/v1/search',{keywords:keywords,}) if(response.data.code===0){if(response.data.data.hash&&response.data.data.hash.length===32){window.location.href="/s/"+response.data.data.hash}else{this.failureAlert('搜索失败: 搜索服务异常')}}else{this.failureAlert('搜索失败: '+response.data.msg)}}catch(error){this.failureAlert('搜索失败: '+error)} this.search.loading=false},debounce(fn,delay){let timer=null return function(...args){if(timer)clearTimeout(timer) timer=setTimeout(()=>{fn.apply(this,args)},delay);};},},watch:{'nav.post.targetPage':{handler:async function(newV,oldV){this.toPage()},immediate:false},},} const vuetify=createVuetify({defaults:{global:{ripple:true,},},}) const app=createApp(App) app.use(vuetify).mount("#app")