Nr(e).format("MMM, YYYY"),nc=e=>Nr(e).format("YYYY"),W0=e=>Nr(e).format("MMM DD, YYYY"),Y0=he({__name:"PostList",props:{list:null,dense:{type:Boolean}},setup(e){return(t,n)=>{const r=U0;return te(),Oe("ul",{class:yt([e.dense?"space-y-5":"space-y-8"])},[(te(!0),Oe(Le,null,Jt(e.list,(i,a)=>(te(),Oe("li",{key:a},[ee(r,{date:ae(W0)(i.date),href:i.path,title:i.title},null,8,["date","href","title"])]))),128))],2)}}}),rc=e=>+Nr(e);function K0(){const e=Gi(),t=ce(()=>e.getRoutes().filter(a=>/\/blog\/(.+)/.test(a.path)).filter(a=>!a.redirect).map(a=>({...a.meta.frontmatter,path:a.path})).filter(a=>!a.draft).sort((a,o)=>rc(o.date)-rc(a.date))),n=ce(()=>{const r=t.value.reduce((i,a)=>{const o=Nr(a.date).year(),s=i[o]??{name:o,posts:[]};return s.posts.push(a),i[o]=s,i},{});return Object.keys(r).sort((i,a)=>Number(a)-Number(i)).map(i=>r[i])});return{posts:t,postGroups:n}}const q0={class:"space-y-8 pt-4"},G0=he({__name:"AboutMeSectionPosts",setup(e){const{posts:t}=K0(),n=ce(()=>t.value.slice(0,2));return(r,i)=>{const a=Y0,o=Ff,s=qn;return te(),ke(s,{title:"Recent Posts"},{default:_e(()=>[Z("div",q0,[ee(a,{dense:"",list:ae(n)},null,8,["list"]),Z("div",null,[ee(o,{href:{name:"posts"}})])])]),_:1})}}}),Q0=he({__name:"LinkIconDemo",setup(e){return(t,n)=>{const r=Ho;return te(),ke(r,{icon:ae(ff),title:"前往 Demo 頁面",onClick:n[0]||(n[0]=fu(()=>{},["stop"]))},null,8,["icon"])}}}),X0=he({__name:"LinkIconGithub",setup(e){return(t,n)=>{const r=Ho;return te(),ke(r,{icon:ae(li),title:"前往 GitHub 頁面",onClick:n[0]||(n[0]=fu(()=>{},["stop"]))},null,8,["icon"])}}}),J0=["src"],Z0=he({__name:"CardProjectImageGlow",props:{img:{default:void 0},sizeClassName:{default:"h-[5.5rem] w-[8rem]"}},setup(e){const t=Ce(!1);async function n(){t.value=!0}return(r,i)=>(te(),Oe("div",{class:yt(["relative before:absolute before:left-0 before:top-0 before:-z-10 before:h-full before:w-full before:rounded-xl before:bg-black-100 after:absolute after:left-0 after:top-0 after:-z-20 after:h-full after:w-full after:overflow-hidden after:rounded-xl after:bg-cover after:bg-center after:blur-lg after:transition-opacity after:duration-500 after:[background-image:var(--img)] after:[will-change:filter] dark:before:bg-black-800",{"after:opacity-0":!t.value,"after:opacity-40":t.value,[e.sizeClassName]:!0}]),style:Hn({"--img":`url(${e.img})`})},[Z("img",{class:yt(["h-full w-full overflow-hidden rounded-xl bg-black-200 object-cover transition-opacity duration-300 dark:bg-black-800",{"opacity-0":!t.value}]),src:e.img,onLoad:n},null,42,J0)],6))}}),e_={};function t_(e,t){const n=Mr;return te(),ke(n,{class:"absolute inset-0 after:pointer-events-none after:absolute after:-inset-3 after:scale-[97%] after:rounded-xl after:bg-black-400/10 after:opacity-0 after:transition-all after:duration-200 hover:after:scale-100 hover:after:opacity-100"})}const n_=Wn(e_,[["render",t_]]),r_={class:"relative flex"},i_={class:"pointer-events-none z-10 mr-5 flex-none"},a_={class:"flex flex-1 flex-col justify-center"},o_={class:"pointer-events-none z-10"},s_={class:"!mb-2 !mt-0 !text-lg !font-medium !leading-5"},l_={class:"!m-0 !text-sm !leading-4 text-black-700 dark:text-black-300"},c_={key:0,class:"mt-3 space-x-2 leading-none"},u_=he({__name:"CardProject",props:{path:null,img:null,title:null,description:null,github:null,link:null},setup(e){return(t,n)=>{const r=n_,i=Z0,a=X0,o=Q0;return te(),Oe("div",r_,[e.path?(te(),ke(r,{key:0,href:e.path},null,8,["href"])):fn("",!0),Z("div",i_,[ee(i,{img:e.img},null,8,["img"])]),Z("div",a_,[Z("div",o_,[Z("h3",s_,Xe(e.title),1),Z("p",l_,Xe(e.description),1)]),e.github||e.link?(te(),Oe("div",c_,[e.github?(te(),ke(a,{key:0,href:e.github},null,8,["href"])):fn("",!0),e.link?(te(),ke(o,{key:1,href:e.link},null,8,["href"])):fn("",!0)])):fn("",!0)])])}}}),f_={class:"grid !list-none grid-cols-1 gap-8 !p-0 md:grid-cols-2"},d_=he({__name:"ProjectList",props:{list:null},setup(e){return(t,n)=>{const r=u_;return te(),Oe("ul",f_,[(te(!0),Oe(Le,null,Jt(e.list,({path:i,title:a,briefDescription:o,githubLink:s,demoLink:l,cover:c},u)=>(te(),Oe("li",{key:u,class:"!p-0"},[ee(r,{description:o,github:s,img:c,link:l,path:i,title:a},null,8,["description","github","img","link","path","title"])]))),128))])}}});function p_(){const e=Gi(),{isDownplayed:t}=Df(),n=ce(()=>e.getRoutes().filter(o=>/\/project\/(.+)/.test(o.path)).filter(o=>!o.redirect).reduce((o,s)=>{var u;const l=(u=s.name)==null?void 0:u.split("project-")[1],c=s.meta.frontmatter;return o[l]={...c,path:s.path,cover:`${c.cover}?v=b795911
+`},o},{})),r=ce(()=>{const a=n.value;return[{title:"Browser Extension",list:[{title:"Taiwan Company Blocker",briefDescription:"台灣求職網封鎖神器",githubLink:"https://github.com/ngseke/taiwan-company-blocker/",demoLink:"https://chromewebstore.google.com/detail/%E5%8F%B0%E7%81%A3%E6%B1%82%E8%81%B7%E7%B6%B2%E5%B0%81%E9%8E%96%E7%A5%9E%E5%99%A8/hmkkfmjfinbllbbkgabkeponkhckmijk",cover:"/img/project-cover/taiwan-company-blocker.png"},a["versatile-npm"],a["leetcode-night"]]},{title:"Web",list:[a["iphone-price"],a.koasu,a.mcip,a["mcip-cms"],a["credit-card-calc"],a["em-optimization-lab"]]},{title:"System Design",list:[a.boss]},{title:"Game",list:[{title:"Flip Card",briefDescription:"Emoji 翻牌配對遊戲",githubLink:"https://github.com/ngseke/vue-flip-card",demoLink:"https://ngseke.github.io/vue-flip-card/",cover:"/img/project-cover/flip-card.png"},a.gomoku,a["tic-tac-toe"],a["raise-your-red-flag"],a["typing-typing"]]},{title:"Identity Design",list:[a["camp-2017"],a.shanlinliang]}]}),i=ce(()=>r.value.filter(a=>t.value?a.title!=="Identity Design":!0));return{projectMap:n,projects:r,downplayedProjects:i}}const m_={class:"space-y-8 pt-4"},h_={class:"pl-1"},g_=he({__name:"AboutMeSectionProjects",setup(e){const{projectMap:t}=p_(),n=[t.value["versatile-npm"],t.value["leetcode-night"]];return(r,i)=>{const a=d_,o=Ff,s=qn;return te(),ke(s,{title:"Projects"},{default:_e(()=>[Z("div",m_,[ee(a,{list:n}),Z("div",h_,[ee(o,{href:{name:"projects"}})])])]),_:1})}}}),v_={class:"flex flex-col"},b_={class:"mr-1 inline-block min-w-[1.5rem] text-center"},__=he({__name:"AboutMeSectionContactMe",setup(e){const t=[bt.github,bt.discord,bt.email,bt.hackmd,bt.linkedin];return(n,r)=>{const i=Bo,a=qn;return te(),ke(a,{title:"Contact Me"},{default:_e(()=>[Z("ul",v_,[(te(),Oe(Le,null,Jt(t,({title:o,url:s,icon:l},c)=>Z("li",{key:c},[ee(i,{href:s},{default:_e(()=>[Z("span",b_,[ee(ae(Pt),{icon:l},null,8,["icon"])]),Re(" "+Xe(o),1)]),_:2},1032,["href"])])),64))])]),_:1})}}}),y_={class:"flex max-w-md flex-col space-y-3 pt-2 leading-tight"},w_={class:"ml-2"},k_={class:"ml-1 mr-3 font-medium"},x_=Z("br",{class:"inline-block sm:hidden"},null,-1),E_={class:"whitespace-nowrap text-base text-black-700 dark:text-black-300"},C_=he({__name:"AboutMeSectionExperience",setup(e){const t=i=>({"flex flex-1 before:mr-1 before:min-w-[.5rem] before:text-center":!0,'opacity-60 before:content-["•"]':i,'before:content-["▸"]':!i}),n=[{title:"Software Engineer",company:"ASUS AICS",period:["2022/07","2023/05"]},{title:"Software Engineer",company:"3drens",period:["2021/08","2022/03"]},{title:"Software Engineer",company:"gogoout",period:["2019/12","2021/07"]},{title:"Summer Engineering Intern",company:"Hiero7",period:"Summer 2018"}],r=i=>{if(typeof i=="string")return i;const a=nc(i[0]),o=i[1]?nc(i[1]):"Present";return`${a} — ${o}`};return(i,a)=>{const o=qn;return te(),ke(o,{title:"Experience"},{default:_e(()=>[Z("ul",y_,[(te(),Oe(Le,null,Jt(n,({title:s,company:l,period:c},u)=>Z("li",{key:u,class:yt(t(u))},[Z("div",w_,[Z("span",null,Xe(s),1),Z("span",k_," @ "+Xe(l),1),x_,Z("span",E_,Xe(r(c)),1)])],2)),64))])]),_:1})}}}),O_={class:"flex max-w-md flex-wrap"},S_={class:"ml-2"},A_=he({__name:"AboutMeSectionSkills",setup(e){const t=["React","Vue 3","Tailwind CSS","Nuxt","TypeScript","Node.js"];return(n,r)=>{const i=qn;return te(),ke(i,{title:"Skills"},{default:_e(()=>[Z("ul",O_,[(te(),Oe(Le,null,Jt(t,(a,o)=>Z("li",{key:o,class:"min-w-[50%] flex-1 before:mr-1 before:inline-block before:origin-right before:opacity-50 before:content-['▸']"},[Z("span",S_,Xe(a),1)])),64))])]),_:1})}}}),T_={},P_={class:"relative not-italic after:absolute after:inset-x-[-1px] after:bottom-[10%] after:h-[20%] after:bg-ngsek/60 dark:after:bg-ngsek/30"},I_={class:"relative z-10"};function M_(e,t){return te(),Oe("em",P_,[Z("span",I_,[Zt(e.$slots,"default")])])}const $_=Wn(T_,[["render",M_]]),L_={},R_={class:"flex flex-col space-y-2"},N_=Z("span",{class:"font-mono text-black-700 dark:text-black-400"}," @ngseke ",-1),D_={class:"text-5xl font-bold sm:text-6xl md:text-7xl"},j_=Re(" Hi, I'm "),F_={class:"whitespace-nowrap"},H_=Re(" Sean"),B_=Re(". 🍻 "),z_=Re(" 現職前端軟體工程師,專注於 "),U_=Re("TypeScript"),V_=Re("、"),W_=Re("Vue"),Y_=Re(" 和 "),K_=Re("React"),q_=Re("。 "),G_=Z("br",null,null,-1),Q_=Re(" 追求撰寫無瑕程式碼是我的開發格言,並致力於優化開發者體驗。 "),X_=Z("br",null,null,-1),J_=Re(" 熱衷探究前端領域的新鮮事,期待藉由知識分享為社群創造影響力。 "),Z_=Z("p",null,[Re(" aka "),Z("span",{class:"underline decoration-dotted",title:"ㄏㄨㄤˊ ㄒㄧㄥˇ ㄑㄧㄠˊ"},"黃省喬"),Re(" / HUANG Sing-Ciao ")],-1);function ey(e,t){const n=$_,r=qn;return te(),ke(r,null,{default:_e(()=>[Z("div",R_,[N_,Z("h2",D_,[j_,Z("span",F_,[ee(n,null,{default:_e(()=>[H_]),_:1}),B_])])]),Z("p",null,[z_,ee(n,null,{default:_e(()=>[U_]),_:1}),V_,ee(n,null,{default:_e(()=>[W_]),_:1}),Y_,ee(n,null,{default:_e(()=>[K_]),_:1}),q_,G_,Q_,X_,J_]),Z_]),_:1})}const ty=Wn(L_,[["render",ey]]),ny={},ry={class:"min-h-screen"},iy={class:"container flex justify-center px-4 py-16"},ay={class:"w-full max-w-3xl space-y-14 text-lg leading-loose"};function oy(e,t){const n=ty,r=A_,i=C_,a=__,o=g_,s=G0;return te(),Oe("div",ry,[Z("div",iy,[Z("article",ay,[ee(n),ee(r),ee(i),ee(a),ee(o),ee(s)])])])}const sy=Wn(ny,[["render",oy]]),ly=e=>(Bd("data-v-b1cdf682"),e=e(),zd(),e),cy={class:"relative flex h-screen w-full flex-col items-center justify-center bg-black-900/20 px-4 md:flex-row"},uy={class:"flex justify-center md:flex-1"},fy={class:"transition-opacity delay-200 duration-700"},dy=ly(()=>Z("h1",{class:"rotate-[-4deg] whitespace-nowrap font-pacifico text-8xl tracking-tight text-white"},[Z("span",{class:"neon blink relative z-10 inline-block after:absolute after:left-0 after:top-0 after:-z-10 after:text-transparent after:content-['ngseke']"}," ngseke ")],-1)),py=[dy],my=he({__name:"CoverBody",setup(e){const t=tb();return(n,r)=>{const i=Uo;return te(),Oe("div",cy,[Z("div",uy,[ee(i,{enterFromClass:"opacity-0"},{default:_e(()=>[pi(Z("div",fy,py,512),[[bi,ae(t)]])]),_:1})])])}}});const hy=Wn(my,[["__scopeId","data-v-b1cdf682"]]),gy="/assets/cover-background-f6446981.jpg",vy=he({__name:"Cover",setup(e){const t={backgroundImage:`url(${gy})`},{coverRef:n}=Rf();return(r,i)=>{const a=hy;return te(),Oe("header",{ref_key:"coverRef",ref:n,class:"flex min-h-screen flex-col justify-center bg-black-900 bg-cover bg-center",style:t},[Zt(r.$slots,"default",{},()=>[ee(a)])],512)}}}),ic="ngseke",s2="https://ngseke.me",l2="img/og",c2="Sean Huang",Bf=he({__name:"index",setup(e){const t=Ir();return Xt(async()=>{t.hash===to&&(await Qt(),Nf({duration:1}))}),eg({title:`${ic}`,meta:[{property:"og:site_name",content:ic}]}),(n,r)=>{const i=vy,a=sy;return te(),Oe(Le,null,[ee(i),ee(a,{id:ae(Wo)},null,8,["id"])],64)}}}),ac={};typeof ac=="function"&&ac(Bf);const by=()=>pe(()=>import("./projects-34cb3ecc.js"),["assets/projects-34cb3ecc.js","assets/useReadHistory-d98c4b5d.js"]),_y=()=>pe(()=>import("./project-e24afe95.js"),["assets/project-e24afe95.js","assets/useOgImage-203f60bd.js","assets/useOgImage-588433c9.css","assets/useReadHistory-d98c4b5d.js"]),yy=()=>pe(()=>import("./versatile-npm-d3f5c477.js"),[]),wy=()=>pe(()=>import("./typingtyping-b132256f.js"),[]),ky=()=>pe(()=>import("./typing-typing-876fd270.js"),[]),xy=()=>pe(()=>import("./tic-tac-toe-dd64aee5.js"),[]),Ey=()=>pe(()=>import("./shanlinliang-9edbff22.js"),[]),Cy=()=>pe(()=>import("./raise-your-red-flag-6df7832f.js"),[]),Oy=()=>pe(()=>import("./mcip-28b770e5.js"),[]),Sy=()=>pe(()=>import("./mcip-cms-dcfd5ebb.js"),[]),Ay=()=>pe(()=>import("./leetcode-night-b75823ff.js"),[]),Ty=()=>pe(()=>import("./koasu-5245e6bf.js"),[]),Py=()=>pe(()=>import("./iphone-price-c267c7d7.js"),[]),Iy=()=>pe(()=>import("./index-95e78139.js"),[]),My=()=>pe(()=>import("./gomoku-e312c04b.js"),[]),$y=()=>pe(()=>import("./flag-5557c288.js"),[]),Ly=()=>pe(()=>import("./emo-5557c288.js"),[]),Ry=()=>pe(()=>import("./em-optimization-lab-d89b3c86.js"),[]),Ny=()=>pe(()=>import("./credit-card-calc-28bb5562.js"),[]),Dy=()=>pe(()=>import("./camp2017-8a21aa39.js"),[]),jy=()=>pe(()=>import("./camp-2017-d632b91c.js"),[]),Fy=()=>pe(()=>import("./boss-7d1971fc.js"),[]),Hy=()=>pe(()=>import("./fe-e736b23f.js"),[]),By=()=>pe(()=>import("./blog-f75762ab.js"),["assets/blog-f75762ab.js","assets/useOgImage-203f60bd.js","assets/useOgImage-588433c9.css"]),zy=()=>pe(()=>import("./wikipedia-link-converter-5743973b.js"),[]),Uy=()=>pe(()=>import("./webpack-config-esm-320be40c.js"),[]),Vy=()=>pe(()=>import("./vite-vue-ts-eslint-setup-1bf5e98d.js"),[]),Wy=()=>pe(()=>import("./typescript-as-const-fb61f8e3.js"),[]),Yy=()=>pe(()=>import("./reproduce-bootstrap-grid-in-tailwind-ce693ef5.js"),[]),Ky=()=>pe(()=>import("./react-handler-type-3a8ff22d.js"),[]),qy=()=>pe(()=>import("./index-4cd242c9.js"),[]),Gy=()=>pe(()=>import("./customize-youtube-caption-font-3e0540da.js"),[]),Qy=()=>pe(()=>import("./component-naming-87122709.js"),[]),Xy=()=>pe(()=>import("./code-review-taxi-number-a7cd2111.js"),[]),Jy=()=>pe(()=>import("./check-if-key-exists-ce0584a7.js"),[]),Zy=()=>pe(()=>import("./bootstrap-in-nuxt-1bb6f56c.js"),[]),e2=()=>pe(()=>import("./about-c7f247d5.js"),[]),t2=()=>pe(()=>import("./404-c7f247d5.js"),[]),n2=()=>pe(()=>import("./_...404_-c7f247d5.js"),[]),r2=[{name:"projects",path:"/projects",component:by,props:!0},{path:"/project",component:_y,children:[{name:"project-versatile-npm",path:"versatile-npm",component:yy,props:!0,meta:{frontmatter:{title:"Versatile Npm",briefDescription:"自訂 Npm 安裝指令瀏覽器擴充功能",githubLink:"https://github.com/ngseke/versatile-npm",demoLink:"https://chromewebstore.google.com/detail/versatile-npm/jahejogdoffpehfhkhbpjblnlhghjnje?hl=zh-TW",period:["2023/11","2023/11"],cover:"/img/project-cover/versatile-npm.png",tags:["Vue","TypeScript","Vuetify","npm"],name:"project"}}},{name:"project-typingtyping",path:"typingtyping",component:wy,props:!0,redirect:{name:"project-typing-typing"}},{name:"project-typing-typing",path:"typing-typing",component:ky,props:!0,meta:{frontmatter:{title:"Typing Typing!",briefDescription:"8-bit 復古風格打字遊戲",githubLink:"https://github.com/ngseke/Typing-Typing",period:["2017/02","2017/06"],members:["余鎧企","黃省喬"],cover:"/img/project-cover/typing-typing.png",tags:["C++"],name:"project"}}},{name:"project-tic-tac-toe",path:"tic-tac-toe",component:xy,props:!0,meta:{frontmatter:{title:"Tic Tac Toe",briefDescription:"圈圈叉叉亂鬥",githubLink:"https://github.com/ngseke/tic-tac-toe",demoLink:"https://ngseke.github.io/tic-tac-toe/",period:["2018/10","2018/10"],cover:"/img/project-cover/tic-tac-toe.png",tags:["Vue","gulp","Firebase","UI/UX"],name:"project"}}},{name:"project-shanlinliang",path:"shanlinliang",component:Ey,props:!0,meta:{frontmatter:{title:"Shanlinliang",briefDescription:"虛構涼扇品牌廣告《扇林涼》",period:["2017/10","2017/10"],cover:"/img/project-cover/shanlinliang.png",tags:["Illustrator"],name:"project"}}},{name:"project-raise-your-red-flag",path:"raise-your-red-flag",component:Cy,props:!0,meta:{frontmatter:{title:"Raise Your Red Flag",briefDescription:"以 Webcam 重現經典團康遊戲《紅旗舉起來》",githubLink:"https://github.com/ngseke/Raise-Your-Red-Flag",demoLink:"https://raise-flag.web.app/",period:["2018/06","2018/06"],cover:"/img/project-cover/flag.png",name:"project"}}},{name:"project-mcip",path:"mcip",component:Oy,props:!0,meta:{frontmatter:{title:"MCIP Official Website",briefDescription:"《樂台計畫》官方網站",githubLink:"https://github.com/ngseke/mcip.ml",demoLink:"https://mcip.app",period:"2019/02",cover:"/img/project-cover/mcip.png",tags:["Nuxt","Schema.org","UI/UX"],name:"project"}}},{name:"project-mcip-cms",path:"mcip-cms",component:Sy,props:!0,meta:{frontmatter:{title:"MCIP CMS",briefDescription:"《樂台計畫》後台管理系統",period:"2018/11",cover:"/img/project-cover/mcip-cms.png",tags:["Vue","Vuetify","ECharts","UI/UX"],name:"project"}}},{name:"project-leetcode-night",path:"leetcode-night",component:Ay,props:!0,meta:{frontmatter:{title:"LeetCode Night",briefDescription:"LeetCode 深色模式瀏覽器擴充功能",githubLink:"https://github.com/ngseke/leetcode-night",demoLink:"https://chrome.google.com/webstore/detail/leetcode-night/aaokgipfeeeciodnffigjfiafledhcii",period:"2022/02",cover:"/img/project-cover/leetcode-night.png",tags:["React","TypeScript","i18n","styled-components","UI/UX"],name:"project"}}},{name:"project-koasu",path:"koasu",component:Ty,props:!0,meta:{frontmatter:{title:"Koasu",briefDescription:"KOASÛ 白話字台語歌詞網誌",githubLink:"https://github.com/ngseke/koasu",demoLink:"https://ngseke.github.io/koasu/",period:"2021/08",cover:"/img/project-cover/koasu.png",tags:["Hexo"],name:"project"}}},{name:"project-iphone-price",path:"iphone-price",component:Py,props:!0,meta:{frontmatter:{title:"iPhone Price",briefDescription:"台灣 iPhone 價格歷史趨勢",githubLink:"https://github.com/ngseke/iphone-price",demoLink:"https://iphone-price.ngseke.me/",period:["2023/10","2023/10"],cover:"/img/project-cover/iphone-price.png",tags:["Vue","TypeScript","Tailwind","daisyUI","ECharts","UI/UX"],name:"project"}}},{name:"project",path:"",component:Iy,props:!0,redirect:{name:"projects"}},{name:"project-gomoku",path:"gomoku",component:My,props:!0,meta:{frontmatter:{title:"Gomoku",briefDescription:"線上五子棋對戰",githubLink:"https://github.com/ngseke/gomoku",demoLink:"https://gomoku.ngseke.me",period:["2018/10","2018/11"],cover:"/img/project-cover/gomoku.png",tags:["Vue","Firebase","UI/UX"],name:"project"}}},{name:"project-flag",path:"flag",component:$y,props:!0,redirect:{name:"project-raise-your-red-flag"}},{name:"project-emo",path:"emo",component:Ly,props:!0,redirect:{name:"project-em-optimization-lab"}},{name:"project-em-optimization-lab",path:"em-optimization-lab",component:Ry,props:!0,meta:{frontmatter:{title:"EM Optimization Lab",briefDescription:"《電磁最佳化實驗室》網站",githubLink:"https://github.com/ngseke/emo",demoLink:"https://myweb.ntut.edu.tw/~yschen/",period:["2018/03","2018/10"],cover:"/img/project-cover/emo.png",tags:["Vue","Firebase","gulp.js","UI/UX"],name:"project"}}},{name:"project-credit-card-calc",path:"credit-card-calc",component:Ny,props:!0,meta:{frontmatter:{title:"Sinopac Dual Currency Card Calc.",briefDescription:"永豐幣倍卡回饋計算機",githubLink:"https://github.com/ngseke/sinopac-dual-currency-card-calculator",demoLink:"https://ngseke.github.io/sinopac-dual-currency-card-calculator/",period:["2021/06","2021/06"],cover:"/img/project-cover/credit-card-calc.png",tags:["Vue","TypeScript","Tailwind CSS","Vite","UI/UX"],name:"project"}}},{name:"project-camp2017",path:"camp2017",component:Dy,props:!0,redirect:{name:"project-camp-2017"}},{name:"project-camp-2017",path:"camp-2017",component:jy,props:!0,meta:{frontmatter:{title:"NTUT-CSIE & NTUB-ACC Camp, 2017",briefDescription:"迎新《會炒不加辣,果資不加糖》品牌識別設計",period:["2017/07","2017/08"],cover:"/img/project-cover/camp2017.png",tags:["Illustrator","Final Cut Pro"],name:"project"}}},{name:"project-boss",path:"boss",component:Fy,props:!0,meta:{frontmatter:{title:"BOSS: Beverage Online Shop System",briefDescription:"線上飲料購物系統",githubLink:"https://github.com/ngseke/boss",period:["2017/11","2018/01"],members:["吳品頤","余鎧企","黃省喬","趙振廷"],cover:"/img/project-cover/boss.png",tags:["PHP","MySQL","UI/UX"],name:"project"}}}],props:!0,name:"project"},{name:"index",path:"/",component:Bf,props:!0},{name:"fe",path:"/fe",component:Hy,props:!0},{name:"post",path:"/blog",component:By,children:[{name:"blog-wikipedia-link-converter",path:"wikipedia-link-converter",component:zy,props:!0,meta:{frontmatter:{title:"還給你 Google 搜尋結果的桌面版維基百科",date:"2022/05/27",tags:["Tampermonkey"],name:"blog"}}},{name:"blog-webpack-config-esm",path:"webpack-config-esm",component:Uy,props:!0,meta:{frontmatter:{title:"在 webpack.config.js 裡使用 ESM 的 import 語法",date:"2022/02/20",tags:["Webpack"],original:"https://hackmd.io/@xq/webpack-config-esm",name:"blog"}}},{name:"blog-vite-vue-ts-eslint-setup",path:"vite-vue-ts-eslint-setup",component:Vy,props:!0,meta:{frontmatter:{title:"建立 Vite + Vue + TypeScript + ESLint 專案可能會遇到的坑",date:"2023/10/03",tags:["Vite","Vue","TypeScript","ESLint"],original:"https://hackmd.io/@xq/vite-vue-ts-eslint-setup",name:"blog"}}},{name:"blog-typescript-as-const",path:"typescript-as-const",component:Wy,props:!0,meta:{frontmatter:{title:"讓 TypeScript 的 as const 救你一命",date:"2021/12/03",tags:["TypeScript"],original:"https://hackmd.io/@xq/as-const",name:"blog"}}},{name:"blog-reproduce-bootstrap-grid-in-tailwind",path:"reproduce-bootstrap-grid-in-tailwind",component:Yy,props:!0,meta:{frontmatter:{title:"用 Tailwind 重現 Bootstrap 的 Grid System",date:"2022/08/22",tags:["Tailwind","Bootstrap"],name:"blog"}}},{name:"blog-react-handler-type",path:"react-handler-type",component:Ky,props:!0,meta:{frontmatter:{title:"處理在 React 抽出 event handler 時常碰到的 TypeScript 參數型別問題",date:"2022/06/06",tags:["React","TypeScript"],name:"blog"}}},{name:"posts",path:"",component:qy,props:!0},{name:"blog-customize-youtube-caption-font",path:"customize-youtube-caption-font",component:Gy,props:!0,meta:{frontmatter:{title:"把 YouTube CC 字幕變成粉圓體",date:"2022/08/14",tags:["Tampermonkey"],name:"blog"}}},{name:"blog-component-naming",path:"component-naming",component:Qy,props:!0,meta:{frontmatter:{title:"試著用有點違反直覺的方式命名組件",date:"2021/11/28",tags:["Vue","React"],original:"https://hackmd.io/@xq/component-naming-reversely",name:"blog"}}},{name:"blog-code-review-taxi-number",path:"code-review-taxi-number",component:Xy,props:!0,meta:{frontmatter:{title:"如果太認真去 Code Review 會發生什麼事?",date:"2023/05/31",tags:["JavaScript"],name:"blog"}}},{name:"blog-check-if-key-exists",path:"check-if-key-exists",component:Jy,props:!0,meta:{frontmatter:{title:"檢查物件的 key 是否存在的 N 種方法",date:"2023/07/07",tags:["JavaScript"],original:"https://hackmd.io/@xq/check-if-key-exist",name:"blog"}}},{name:"blog-bootstrap-in-nuxt",path:"bootstrap-in-nuxt",component:Zy,props:!0,meta:{frontmatter:{title:"在 Nuxt 專案穩穩地匯入 Bootstrap",date:"2021/11/21",tags:["Nuxt","Vue","Bootstrap"],original:"https://hackmd.io/@xq/bootstrap-with-nuxt",name:"blog"}}}],props:!0},{name:"about",path:"/about",component:e2,props:!0,redirect:{name:"index",hash:"#about"}},{name:"404",path:"/404",component:t2,props:!0,redirect:{name:"index"}},{name:"404",path:"/:404(.*)*",component:n2,props:!0,redirect:{name:"index"}}];ag(R0,{routes:r2,scrollBehavior(e,t,n){if(![e.name,t.name].every(r=>r==="projects"))return n||{top:0}}});export{Bd as $,a2 as A,Gi as B,Xt as C,W0 as D,Pt as E,Le as F,n1 as G,c2 as H,Yn as I,vo as J,je as K,Qt as L,mb as M,ab as N,yt as O,ri as P,Ce as Q,ft as R,i2 as S,Yi as T,Vc as U,ic as V,pi as W,Pr as X,Op as Y,Z0 as Z,Wn as _,Oe as a,zd as a0,s2 as a1,l2 as a2,K0 as a3,Y0 as a4,fn as b,ke as c,he as d,eg as e,p_ as f,Df as g,Z as h,Jt as i,ee as j,Re as k,d_ as l,ac as m,Mr as n,te as o,Ir as p,ce as q,Zt as r,o2 as s,Xe as t,ae as u,Wc as v,_e as w,X0 as x,Q0 as y,eb as z};
diff --git a/assets/banner-63e3ec60.png b/assets/banner-63e3ec60.png
new file mode 100644
index 00000000..e49bb173
Binary files /dev/null and b/assets/banner-63e3ec60.png differ
diff --git a/assets/before-485cea7e.jpg b/assets/before-485cea7e.jpg
new file mode 100644
index 00000000..851fb2c4
Binary files /dev/null and b/assets/before-485cea7e.jpg differ
diff --git a/assets/before-7a436972.png b/assets/before-7a436972.png
new file mode 100644
index 00000000..c7303f46
Binary files /dev/null and b/assets/before-7a436972.png differ
diff --git a/assets/blog-f75762ab.js b/assets/blog-f75762ab.js
new file mode 100644
index 00000000..34bf6392
--- /dev/null
+++ b/assets/blog-f75762ab.js
@@ -0,0 +1 @@
+import{u as O,_ as $,b as A}from"./useOgImage-203f60bd.js";import{d as I,o as s,a as r,F as R,i as D,t as u,b as f,p as E,q as c,V as w,e as S,D as j,v as T,u as t,c as p,w as g,j as i,h as o,E as M,G as q,k as G,H as U,m as V}from"./app-033a95d9.js";const z={key:0,class:"mb-2 flex space-x-2 text-lg font-semibold text-ngsek"},J=I({__name:"PostTags",props:{list:null},setup(l){return(d,e)=>l.list?(s(),r("ul",z,[(s(!0),r(R,null,D(l.list,(n,_)=>(s(),r("li",{key:_,class:"before:content-['#']"},u(n),1))),128))])):f("",!0)}});function K(){const l=E();return{frontmatter:c(()=>{var e;return(e=l.meta)==null?void 0:e.frontmatter})}}const Q={class:"mb-6 text-3xl font-semibold md:text-4xl md:leading-tight"},W={class:"flex space-x-1 text-sm text-black-700 dark:text-black-300"},X=o("span",{class:'opacity-60 before:content-["•"]'},null,-1),Y={key:3,class:"container px-4 pb-8"},Z={class:"mx-auto max-w-3xl"},tt=o("hr",{class:"mb-8 border-dashed border-black-200 dark:border-black-800"},null,-1),et={class:"flex flex-wrap"},ot=G(" 閱讀更多文章 "),st=I({__name:"blog",setup(l){const d=E(),{frontmatter:e}=K(),n=c(()=>{var a;return`${((a=e.value)==null?void 0:a.title)??"Blog"} | ${w}`}),{ogImageHeadMetaList:_,shouldShowOgImage:L}=O();S(c(()=>({title:n.value,description:n.value,meta:[{property:"og:site_name",content:w},{property:"og:title",content:n.value},{property:"description",content:n.value},{property:"og:description",content:n.value},..._.value]})));const B=c(()=>{var m;const a=(m=e.value)==null?void 0:m.date;return a?j(a):null}),C=U,b=c(()=>d.name==="posts");return(a,m)=>{var h,k;const F=$,x=T("RouterView"),N=J,P=A,H=T("RouterLink");return s(),r(R,null,[t(L)?(s(),p(F,{key:0,date:(h=t(e))==null?void 0:h.date,title:(k=t(e))==null?void 0:k.title},null,8,["date","title"])):f("",!0),t(b)?(s(),p(x,{key:1})):(s(),p(P,{key:2},{header:g(()=>{var y,v;return[i(N,{list:(y=t(e))==null?void 0:y.tags},null,8,["list"]),o("h1",Q,u((v=t(e))==null?void 0:v.title),1),o("ul",W,[o("li",null,u(t(B)),1),X,o("li",null,"by "+u(t(C)),1)])]}),default:g(()=>[i(x)]),_:1})),t(b)?f("",!0):(s(),r("div",Y,[o("div",Z,[tt,o("div",et,[i(H,{class:"inline-flex w-full justify-between rounded-lg bg-black-400/10 p-6 font-semibold leading-none tracking-wider hover:bg-black-400/20 md:w-1/2",to:{name:"posts"}},{default:g(()=>[ot,i(t(M),{icon:t(q)},null,8,["icon"])]),_:1})])])]))],64)}}});typeof V=="function"&&V(st);export{st as default};
diff --git a/assets/bootstrap-in-nuxt-1bb6f56c.js b/assets/bootstrap-in-nuxt-1bb6f56c.js
new file mode 100644
index 00000000..08c286c8
--- /dev/null
+++ b/assets/bootstrap-in-nuxt-1bb6f56c.js
@@ -0,0 +1,117 @@
+import{d as e,v as t,o as c,a as r,h as s,j as o,k as a,A as l}from"./app-033a95d9.js";const i={class:"markdown-body"},y=s("blockquote",null,[s("p",null,[a("此筆記指的是純 "),s("a",{href:"https://github.com/twbs/bootstrap"},"bootstrap"),a(",與 "),s("a",{href:"https://github.com/bootstrap-vue/bootstrap-vue"},"bootstrap-vue"),a(" 無關。")])],-1),A=s("blockquote",null,[s("p",null,[a("下文出現的 "),s("strong",null,"BS"),a(" 是 Bootstrap 的簡寫")])],-1),d={id:"%F0%9F%8C%9F-%E9%80%99%E7%AF%87%E7%AD%86%E8%A8%98%E7%9A%84%E9%87%8D%E9%BB%9E%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F",tabindex:"-1"},B=a("🌟 這篇筆記的重點有哪些? "),h={class:"header-anchor",href:"#%F0%9F%8C%9F-%E9%80%99%E7%AF%87%E7%AD%86%E8%A8%98%E7%9A%84%E9%87%8D%E9%BB%9E%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F"},u=s("p",null,"與直接匯入編譯過並打包好的 css 相比,此作法:",-1),D=s("ol",null,[s("li",null,"保留自定義樣式的能力,優雅地從根源自定義主題,免去用層層的自訂 class 複寫"),s("li",null,[a("避免組件充斥著 "),s("code",{class:""},"@media (min-width: 576px) { ... }"),a(" 諸如此類的魔法數字")]),s("li",null,"節省專案建構大小")],-1),E={id:"%F0%9F%94%A7-%E5%AE%89%E8%A3%9D-bootstrap",tabindex:"-1"},_=a("🔧 安裝 Bootstrap "),b={class:"header-anchor",href:"#%F0%9F%94%A7-%E5%AE%89%E8%A3%9D-bootstrap"},C=l(`npm i bootstrap
+
npm i bootstrap
+
`,1),m={id:"%F0%9F%94%A7-%E5%AE%89%E8%A3%9D-%40nuxtjs%2Fstyle-resources",tabindex:"-1"},F=a("🔧 安裝 "),g=s("code",{class:""},"@nuxtjs/style-resources",-1),k=a(),f={class:"header-anchor",href:"#%F0%9F%94%A7-%E5%AE%89%E8%A3%9D-%40nuxtjs%2Fstyle-resources"},q=l(`npm i @nuxtjs/style-resources
+
npm i @nuxtjs/style-resources
+
`,1),x={id:"%F0%9F%93%83-%E5%BB%BA%E7%AB%8B-_bootstrap.sass",tabindex:"-1"},v=a("📃 建立 "),S=s("code",{class:""},"_bootstrap.sass",-1),w=a(),j={class:"header-anchor",href:"#%F0%9F%93%83-%E5%BB%BA%E7%AB%8B-_bootstrap.sass"},$=l(`建立 assets/sass/style-resources/_bootstrap.sass
(檔名隨意)
在裡面匯入 BS 的源 SCSS 檔案,這三行是必須的。
// _bootstrap.sass
+
+@import "~bootstrap/scss/functions"
+@import "~bootstrap/scss/variables"
+@import "~bootstrap/scss/mixins"
+
// _bootstrap.sass
+
+@import "~bootstrap/scss/functions"
+@import "~bootstrap/scss/variables"
+@import "~bootstrap/scss/mixins"
+
https://getbootstrap.com/docs/4.6/getting-started/theming/#importing
`,4),N={id:"%F0%9F%93%83-%E5%BB%BA%E7%AB%8B-_custom.sass",tabindex:"-1"},V=a("📃 建立 "),R=s("code",{class:""},"_custom.sass",-1),T=a(),z={class:"header-anchor",href:"#%F0%9F%93%83-%E5%BB%BA%E7%AB%8B-_custom.sass"},G=l(`建立 assets/sass/style-resources/_custom.sass
(檔名隨意)。
此檔用來複寫 BS 預設變數。
// _custom.sass
+
+// 🌰 舉例:
+
+// 將「primary」換成黃色
+$theme-colors : ( primary : #ffd019 )
+
+// 將連結換成黃色,並移除 hover 的底線
+$link-color : #ffd019
+$link-hover-decoration : none
+
+// 重新定義斷點大小
+$grid-breakpoints : ( xs : 0 , sm : 576 px , md : 768 px , lg : 992 px , xl : 1200 px , xxl : 1400 px )
+
+/* ... */
+
// _custom.sass
+
+// 🌰 舉例:
+
+// 將「primary」換成黃色
+$theme-colors : ( primary : #ffd019 )
+
+// 將連結換成黃色,並移除 hover 的底線
+$link-color : #ffd019
+$link-hover-decoration : none
+
+// 重新定義斷點大小
+$grid-breakpoints : ( xs : 0 , sm : 576 px , md : 768 px , lg : 992 px , xl : 1200 px , xxl : 1400 px )
+
+/* ... */
+
更多可供複寫的變數都列在此處:https://github.com/twbs/bootstrap/blob/main/scss/_variables.scss
`,4),H={id:"%F0%9F%93%83-%E5%BB%BA%E7%AB%8B-style.sass",tabindex:"-1"},I=a("📃 建立 "),J=s("code",{class:""},"style.sass",-1),K=a(),L={class:"header-anchor",href:"#%F0%9F%93%83-%E5%BB%BA%E7%AB%8B-style.sass"},M=l(`建立 assets/sass/style.sass
(檔名隨意)
根據各自需求匯入 BS 元件,這些元件將會套用前面設定的主題變數。
// style.sass
+
+// 👍 推薦的元件
+@import "~bootstrap/scss/reboot"
+@import "~bootstrap/scss/utilities"
+@import "~bootstrap/scss/grid"
+@import "~bootstrap/scss/type"
+
+// 其他常用元件
+@import "~bootstrap/scss/images"
+@import "~bootstrap/scss/nav"
+@import "~bootstrap/scss/navbar"
+
+/* ... */
+
// style.sass
+
+// 👍 推薦的元件
+@import "~bootstrap/scss/reboot"
+@import "~bootstrap/scss/utilities"
+@import "~bootstrap/scss/grid"
+@import "~bootstrap/scss/type"
+
+// 其他常用元件
+@import "~bootstrap/scss/images"
+@import "~bootstrap/scss/nav"
+@import "~bootstrap/scss/navbar"
+
+/* ... */
+
可用的元件列表及用途請參考此處:https://github.com/twbs/bootstrap/tree/main/scss
`,4),O={id:"%E2%9C%8F%EF%B8%8F-%E9%85%8D%E7%BD%AE-nuxt.config.js",tabindex:"-1"},P=a("✏️ 配置 "),Q=s("code",{class:""},"nuxt.config.js",-1),U=a(),W={class:"header-anchor",href:"#%E2%9C%8F%EF%B8%8F-%E9%85%8D%E7%BD%AE-nuxt.config.js"},X=l(`export default {
+ /* ... */
+
+ styleResources : {
+ sass : [
+ './assets/sass/style-resources/_custom.sass' ,
+ './assets/sass/style-resources/_bootstrap.sass' ,
+ ],
+ },
+
+ css : [
+ './assets/sass/style.sass' ,
+ ],
+}
+
export default {
+ /* ... */
+
+ styleResources : {
+ sass : [
+ './assets/sass/style-resources/_custom.sass' ,
+ './assets/sass/style-resources/_bootstrap.sass' ,
+ ],
+ },
+
+ css : [
+ './assets/sass/style.sass' ,
+ ],
+}
+
⚠️ 注意 是在 css
匯入 style.sass
,而不是在styleResources
匯入。 在 styleResources
匯入的話,將使得每個 Vue 組件 都包含一大包的 Bootstrap 樣式,造成專案容量爆肥,編譯速度也會被嚴重拖慢。
`,2),Y={id:"%F0%9F%96%96-%E5%9C%A8-vue-%E7%B5%84%E4%BB%B6%E5%85%A7%E4%BD%BF%E7%94%A8",tabindex:"-1"},Z=a("🖖 在 Vue 組件內使用 "),ss={class:"header-anchor",href:"#%F0%9F%96%96-%E5%9C%A8-vue-%E7%B5%84%E4%BB%B6%E5%85%A7%E4%BD%BF%E7%94%A8"},as=l(`🎉 大功告成!
如此便可自由自在地在 vue 組件內使用 BS 的 class 和所有 SCSS/SASS 變數。
< template > /* ... */ </ template >
+< script > /* ... */ </ script >
+
+< style lang = "sass" scoped >
+div
+ @include media-breakpoint-down(sm)
+ background: $primary
+</ style >
+
< template > /* ... */ </ template >
+< script > /* ... */ </ script >
+
+< style lang = "sass" scoped >
+div
+ @include media-breakpoint-down(sm)
+ background: $primary
+</ style >
+
`,3),es={title:"在 Nuxt 專案穩穩地匯入 Bootstrap",date:"2021/11/21",tags:["Nuxt","Vue","Bootstrap"],original:"https://hackmd.io/@xq/bootstrap-with-nuxt"},ts="",cs=e({__name:"bootstrap-in-nuxt",setup(ns,{expose:p}){return p({frontmatter:{title:"在 Nuxt 專案穩穩地匯入 Bootstrap",date:"2021/11/21",tags:["Nuxt","Vue","Bootstrap"],original:"https://hackmd.io/@xq/bootstrap-with-nuxt"},excerpt:void 0}),(os,ls)=>{const n=t("icon-link");return c(),r("div",i,[y,A,s("h2",d,[B,s("a",h,[o(n)])]),u,D,s("h2",E,[_,s("a",b,[o(n)])]),C,s("h2",m,[F,g,k,s("a",f,[o(n)])]),q,s("h2",x,[v,S,w,s("a",j,[o(n)])]),$,s("h2",N,[V,R,T,s("a",z,[o(n)])]),G,s("h2",H,[I,J,K,s("a",L,[o(n)])]),M,s("h2",O,[P,Q,U,s("a",W,[o(n)])]),X,s("h2",Y,[Z,s("a",ss,[o(n)])]),as])}}});export{cs as default,ts as excerpt,es as frontmatter};
diff --git a/assets/boss-7d1971fc.js b/assets/boss-7d1971fc.js
new file mode 100644
index 00000000..5ee65d44
--- /dev/null
+++ b/assets/boss-7d1971fc.js
@@ -0,0 +1 @@
+import{d as r,v as c,o as i,a,h as s,j as o,k as e}from"./app-033a95d9.js";const _="/assets/boss1-d440988b.png",p="/assets/boss2-3442c269.png",h="/assets/boss3-5ed82877.png",m="/assets/boss4-2c92bcf2.png",d="/assets/boss5-c63dc0c0.png",l={class:"markdown-body"},b=s("p",null,"這是一個在線飲料購物系統 (Beverage Online Shop System),簡稱BOSS,是一個簡單易用,介面時尚清爽的飲料購物平台。本專案後端以PHP語言撰寫,前端以Bootstrap的架構搭配HTML5和CSS3作為基礎進行版型設計。",-1),g=s("p",null,[s("img",{src:_,alt:""})],-1),u=s("p",null,[e("在此系統中可以根據不同使用者身份,包括顧客、員工及管理員,提供各種不同的操作功能。"),s("br"),e(" 我們提供簡單直覺易懂的操作界面,讓顧客可以瀏覽本店所有商品、根據分類或搜尋條件取得商品資訊、對於產品及訂單進行打分或評論、查看。"),s("br"),e(" 員工可以上下架商品、管理商品之庫存及資訊、查看當前所有訂單資訊。管理者可以新增移除或修改會員資訊。")],-1),B={id:"%E4%BB%8B%E9%9D%A2",tabindex:"-1"},S=e("介面 "),f={class:"header-anchor",href:"#%E4%BB%8B%E9%9D%A2"},k=s("p",null,[s("img",{src:p,alt:"隨機展示熱銷商品"}),s("br"),s("img",{src:h,alt:"顯示當前優惠活動"}),s("br"),s("img",{src:m,alt:"商品列表"}),s("br"),s("img",{src:d,alt:"商品詳情頁面"})],-1),v={id:"demo",tabindex:"-1"},x=e("Demo "),y={class:"header-anchor",href:"#demo"},O=s("p",null,[s("s",null,[s("a",{href:"http://boss.ngseke.me/"},"http://boss.ngseke.me/")]),e(" 已下架")],-1),P=s("iframe",{src:"https://ghbtns.com/github-btn.html?user=ngseke&repo=boss&type=star&count=false",frameborder:"0",scrolling:"0",width:"150",height:"20"},null,-1),U={title:"BOSS: Beverage Online Shop System",briefDescription:"線上飲料購物系統",githubLink:"https://github.com/ngseke/boss",period:["2017/11","2018/01"],members:["吳品頤","余鎧企","黃省喬","趙振廷"],cover:"/img/project-cover/boss.png",tags:["PHP","MySQL","UI/UX"]},j="",C=r({__name:"boss",setup(D,{expose:n}){return n({frontmatter:{title:"BOSS: Beverage Online Shop System",briefDescription:"線上飲料購物系統",githubLink:"https://github.com/ngseke/boss",period:["2017/11","2018/01"],members:["吳品頤","余鎧企","黃省喬","趙振廷"],cover:"/img/project-cover/boss.png",tags:["PHP","MySQL","UI/UX"]},excerpt:void 0}),(E,L)=>{const t=c("icon-link");return i(),a("div",l,[b,g,u,s("h2",B,[S,s("a",f,[o(t)])]),k,s("h2",v,[x,s("a",y,[o(t)])]),O,P])}}});export{C as default,j as excerpt,U as frontmatter};
diff --git a/assets/boss1-d440988b.png b/assets/boss1-d440988b.png
new file mode 100644
index 00000000..1a51e4fd
Binary files /dev/null and b/assets/boss1-d440988b.png differ
diff --git a/assets/boss2-3442c269.png b/assets/boss2-3442c269.png
new file mode 100644
index 00000000..53355304
Binary files /dev/null and b/assets/boss2-3442c269.png differ
diff --git a/assets/boss3-5ed82877.png b/assets/boss3-5ed82877.png
new file mode 100644
index 00000000..50f58134
Binary files /dev/null and b/assets/boss3-5ed82877.png differ
diff --git a/assets/boss4-2c92bcf2.png b/assets/boss4-2c92bcf2.png
new file mode 100644
index 00000000..03742796
Binary files /dev/null and b/assets/boss4-2c92bcf2.png differ
diff --git a/assets/boss5-c63dc0c0.png b/assets/boss5-c63dc0c0.png
new file mode 100644
index 00000000..b7f7fd2f
Binary files /dev/null and b/assets/boss5-c63dc0c0.png differ
diff --git a/assets/bumu1-2902b5f3.png b/assets/bumu1-2902b5f3.png
new file mode 100644
index 00000000..03c46dec
Binary files /dev/null and b/assets/bumu1-2902b5f3.png differ
diff --git a/assets/bumu2-9fbc6c06.jpg b/assets/bumu2-9fbc6c06.jpg
new file mode 100644
index 00000000..07b7c71b
Binary files /dev/null and b/assets/bumu2-9fbc6c06.jpg differ
diff --git a/assets/camp-2017-d632b91c.js b/assets/camp-2017-d632b91c.js
new file mode 100644
index 00000000..4dfec138
--- /dev/null
+++ b/assets/camp-2017-d632b91c.js
@@ -0,0 +1 @@
+import{d as n,v as r,o as _,a as d,h as s,j as e,k as t,A as c}from"./app-033a95d9.js";const a="/assets/logo_fb-3a5fbf8a.png",p="/assets/icon-3f390287.png",m="/assets/bumu1-2902b5f3.png",l="/assets/logo_en-8478642b.png",h="/assets/bumu2-9fbc6c06.jpg",g="/assets/mingpai1-50edfb95.png",b="/assets/mingpai2-dead019a.png",E="/assets/tshirt1-4f8fb94c.png",f="/assets/tshirt2-c61c550c.png",B="/assets/hengfu-324e5744.png",A="/assets/fb_zhuanye-8a3106ac.png",u="/assets/tixing-f836dc82.png",x="/assets/dahezhao-4534134f.jpg",C="/assets/datoutie1-f730def3.png",D="/assets/datoutie2-b626c0ae.png",v="/assets/datoutie3-99933bc0.png",w="/assets/datoutie4-8d7f399d.png",k="/assets/datoutie_fb-b37840ad.png",T="/assets/0-11090461.png",F="/assets/1-f5f11eb3.png",N="/assets/2-a067597c.png",j="/assets/3-78ab6d35.png",I="/assets/4-708e3caa.png",y="/assets/5-0224294b.png",L="/assets/6-a159bc1d.png",V="/assets/7-cb414675.png",z="/assets/shouce1-193ed0a8.jpg",S="/assets/shouce2-0a3f097e.jpg",U={class:"markdown-body"},P=s("p",null,[s("img",{src:a,alt:""})],-1),q=s("p",null,[t("2017 年秋季"),s("em",null,"北科資工系"),t("和"),s("em",null,"北商會資系"),t("聯合舉辦的迎新活動,使用 Adobe Illustrator 設計與排版,圖標源自 "),s("a",{href:"https://www.flaticon.com"},"Flaticon"),t("。")],-1),G={id:"logo",tabindex:"-1"},H=t("Logo "),K={class:"header-anchor",href:"#logo"},Q=s("p",null,"Logo 分別展現出快炒鍋、辣椒、果汁和方糖的意象,與活動名稱相呼應。",-1),R=s("p",null,[s("img",{src:p,alt:"Logo圖標"}),s("br"),s("img",{src:m,alt:"Logo文字"}),s("br"),s("img",{src:l,alt:"英文版Logo文字"}),s("br"),s("img",{src:h,alt:"布幕"})],-1),Y={id:"%E5%90%8D%E7%89%8C",tabindex:"-1"},J=t("名牌 "),M={class:"header-anchor",href:"#%E5%90%8D%E7%89%8C"},O=s("p",null,[s("img",{src:g,alt:"名牌"}),s("br"),s("img",{src:b,alt:"名牌(英文)"})],-1),W={id:"t-shirt",tabindex:"-1"},X=t("T-SHIRT "),Z={class:"header-anchor",href:"#t-shirt"},$=s("p",null,[s("img",{src:E,alt:"營T"}),s("br"),s("img",{src:f,alt:"組T(背後)"})],-1),ss={id:"facebook-%E7%B2%89%E5%B0%88",tabindex:"-1"},ts=t("Facebook 粉專 "),os={class:"header-anchor",href:"#facebook-%E7%B2%89%E5%B0%88"},es=c('
',1),cs={id:"facebook-%E5%A4%A7%E9%A0%AD%E8%B2%BC",tabindex:"-1"},as=t("Facebook 大頭貼 "),is={class:"header-anchor",href:"#facebook-%E5%A4%A7%E9%A0%AD%E8%B2%BC"},ns=c('供工人宣傳期間替換的大頭貼,另有無頭版本可自由定制。
',1),rs={id:"%E5%B0%8F%E6%89%8B%E5%86%8A",tabindex:"-1"},_s=t("小手冊 "),ds={class:"header-anchor",href:"#%E5%B0%8F%E6%89%8B%E5%86%8A"},ps=c('
',2),ms={id:"%E5%9B%9E%E9%A1%A7%E5%BD%B1%E7%89%87",tabindex:"-1"},ls=t("回顧影片 "),hs={class:"header-anchor",href:"#%E5%9B%9E%E9%A1%A7%E5%BD%B1%E7%89%87"},gs=s("p",null,"使用 Final Cut Pro 剪輯。",-1),bs={id:"%E5%B7%A5%E4%BA%BA%E5%9B%9E%E9%A1%A7%E5%BD%B1%E7%89%87",tabindex:"-1"},Es=t("工人回顧影片 "),fs={class:"header-anchor",href:"#%E5%B7%A5%E4%BA%BA%E5%9B%9E%E9%A1%A7%E5%BD%B1%E7%89%87"},Bs=s("div",{class:"embed-responsive"},[s("iframe",{class:"embed-responsive-item",src:"https://www.youtube.com/embed/xfxfVKGzyqY",allowfullscreen:""})],-1),As={id:"%E4%B8%89%E6%97%A5%E5%AD%B8%E5%93%A1%E5%9B%9E%E9%A1%A7%E5%BD%B1%E7%89%87",tabindex:"-1"},us=t("三日學員回顧影片 "),xs={class:"header-anchor",href:"#%E4%B8%89%E6%97%A5%E5%AD%B8%E5%93%A1%E5%9B%9E%E9%A1%A7%E5%BD%B1%E7%89%87"},Cs=s("div",{class:"embed-responsive"},[s("iframe",{class:"embed-responsive-item",src:"https://www.youtube.com/embed/57wQDmziAEw",allowfullscreen:""})],-1),Ts={title:"NTUT-CSIE & NTUB-ACC Camp, 2017",briefDescription:"迎新《會炒不加辣,果資不加糖》品牌識別設計",period:["2017/07","2017/08"],cover:"/img/project-cover/camp2017.png",tags:["Illustrator","Final Cut Pro"]},Fs="",Ns=n({__name:"camp-2017",setup(Ds,{expose:i}){return i({frontmatter:{title:"NTUT-CSIE & NTUB-ACC Camp, 2017",briefDescription:"迎新《會炒不加辣,果資不加糖》品牌識別設計",period:["2017/07","2017/08"],cover:"/img/project-cover/camp2017.png",tags:["Illustrator","Final Cut Pro"]},excerpt:void 0}),(vs,ws)=>{const o=r("icon-link");return _(),d("div",U,[P,q,s("h2",G,[H,s("a",K,[e(o)])]),Q,R,s("h2",Y,[J,s("a",M,[e(o)])]),O,s("h2",W,[X,s("a",Z,[e(o)])]),$,s("h2",ss,[ts,s("a",os,[e(o)])]),es,s("h2",cs,[as,s("a",is,[e(o)])]),ns,s("h2",rs,[_s,s("a",ds,[e(o)])]),ps,s("h2",ms,[ls,s("a",hs,[e(o)])]),gs,s("h3",bs,[Es,s("a",fs,[e(o)])]),Bs,s("h3",As,[us,s("a",xs,[e(o)])]),Cs])}}});export{Ns as default,Fs as excerpt,Ts as frontmatter};
diff --git a/assets/camp2017-8a21aa39.js b/assets/camp2017-8a21aa39.js
new file mode 100644
index 00000000..f0f7fe5e
--- /dev/null
+++ b/assets/camp2017-8a21aa39.js
@@ -0,0 +1 @@
+import{m as o}from"./app-033a95d9.js";const f={};typeof o=="function"&&o(f);export{f as default};
diff --git a/assets/char2-9248d6c0.png b/assets/char2-9248d6c0.png
new file mode 100644
index 00000000..d89a68d7
Binary files /dev/null and b/assets/char2-9248d6c0.png differ
diff --git a/assets/char3-5ba16f0d.png b/assets/char3-5ba16f0d.png
new file mode 100644
index 00000000..6c4a0d38
Binary files /dev/null and b/assets/char3-5ba16f0d.png differ
diff --git a/assets/check-if-key-exists-ce0584a7.js b/assets/check-if-key-exists-ce0584a7.js
new file mode 100644
index 00000000..fb7e6823
--- /dev/null
+++ b/assets/check-if-key-exists-ce0584a7.js
@@ -0,0 +1,133 @@
+import{d as e,v as c,o as t,a as r,h as s,j as l,k as n,A as p}from"./app-033a95d9.js";const y="/assets/valueOf-956b83dd.png",i={class:"markdown-body"},A={id:".hasownproperty()",tabindex:"-1"},B=s("code",{class:""},".hasOwnProperty()",-1),d=n(),D={class:"header-anchor",href:"#.hasownproperty()"},C=p(`使用物件的 prototype hasOwnProperty
檢查。
不會 遍歷原型鏈。
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+}
+
+profile . hasOwnProperty ( 'name' ) // true
+profile . hasOwnProperty ( 'valueOf' ) // false
+
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+}
+
+profile . hasOwnProperty ( 'name' ) // true
+profile . hasOwnProperty ( 'valueOf' ) // false
+
`,3),h={id:"in",tabindex:"-1"},f=s("code",{class:""},"in",-1),E=n(),F={class:"header-anchor",href:"#in"},_=p(`最簡潔的寫法,使用關鍵字 in
檢查。
但請留意 in
會 去尋找整個原型鏈,可能得到預期外的結果,並造成微乎其微的效能差異。
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+}
+
+'name' in profile // true
+
+/*
+ ⚠️ 雖然以下 key 並沒有在 \`profile\` 中明確定義,
+ 但因為它們存在於物件的 prototype 中,所以依然會得到 true。
+*/
+'valueOf' in profile // true
+'toString' in profile // true
+'hasOwnProperty' in profile // true
+
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+}
+
+'name' in profile // true
+
+/*
+ ⚠️ 雖然以下 key 並沒有在 \`profile\` 中明確定義,
+ 但因為它們存在於物件的 prototype 中,所以依然會得到 true。
+*/
+'valueOf' in profile // true
+'toString' in profile // true
+'hasOwnProperty' in profile // true
+
效能實測:hasOwnProperty 和 in 哪个性能高? - frosen的回答 - 知乎
',5),k={id:"object.prototype.hasownproperty.call()",tabindex:"-1"},u=s("code",{class:""},"Object.prototype.hasOwnProperty.call()",-1),g=n(),b={class:"header-anchor",href:"#object.prototype.hasownproperty.call()"},O=p(`由於 JavaScript 未保護 hasOwnProperty
,所以你完全可以複寫這個屬性,讓它刻意回傳錯誤的結果。
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+ hasOwnProperty : () => true ,
+}
+
+/* 😢 現在無論傳入什麼都會得到 true */
+profile . hasOwnProperty ( '🍺' ) // true
+profile . hasOwnProperty ( 123 ) // true
+
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+ hasOwnProperty : () => true ,
+}
+
+/* 😢 現在無論傳入什麼都會得到 true */
+profile . hasOwnProperty ( '🍺' ) // true
+profile . hasOwnProperty ( 123 ) // true
+
利用 Object.prototype.hasOwnProperty.call()
即可避免此情況發生,也不會 遍歷原型鏈。
/* 😎 雖然冗長但最安全 */
+Object . prototype . hasOwnProperty . call ( profile , 'name' ) // true
+Object . prototype . hasOwnProperty . call ( profile , '🍺' ) // false
+
/* 😎 雖然冗長但最安全 */
+Object . prototype . hasOwnProperty . call ( profile , 'name' ) // true
+Object . prototype . hasOwnProperty . call ( profile , '🍺' ) // false
+
`,4),m={id:"object.hasown()",tabindex:"-1"},v=s("code",{class:""},"Object.hasOwn()",-1),w=n(),j={class:"header-anchor",href:"#object.hasown()"},S=p(`ES13 推出的新特性,旨在取代 Object.prototype.hasOwnProperty()
,寫相比之下法更直觀和簡潔。並且和 Object.prototype.hasOwnProperty.call()
一樣,即使複寫了 hasOwnProperty
依然可以得到正確的結果。
不會 遍歷原型鏈。
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+ hasOwnProperty : () => true ,
+}
+
+Object . hasOwn ( profile , 'name' ) // true
+Object . hasOwn ( profile , '🍺' ) // false
+
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+ hasOwnProperty : () => true ,
+}
+
+Object . hasOwn ( profile , 'name' ) // true
+Object . hasOwn ( profile , '🍺' ) // false
+
`,3),P={id:"%E5%85%B6%E4%BB%96%E5%B8%B8%E8%A6%8B%E4%BD%86%E6%9C%89%E9%99%B7%E9%98%B1%E7%9A%84%E6%96%B9%E6%B3%95",tabindex:"-1"},x=n("其他常見但有陷阱的方法 "),z={class:"header-anchor",href:"#%E5%85%B6%E4%BB%96%E5%B8%B8%E8%A6%8B%E4%BD%86%E6%9C%89%E9%99%B7%E9%98%B1%E7%9A%84%E6%96%B9%E6%B3%95"},J={id:"object.keys().includes()-%F0%9F%98%90",tabindex:"-1"},N=s("code",{class:""},"Object.keys().includes()",-1),q=n(" 😐 "),G={class:"header-anchor",href:"#object.keys().includes()-%F0%9F%98%90"},R=p(`先以 Object.keys()
取得物件的所有 key 的陣列,接著呼叫陣列的方法 includes()
來檢查 key 是否存在。
但這個方法的時間複雜度是 O(n)
(或更高 ),因為它必須先至少遍歷物件來得到所有的 key,接著尋找時又得再遍歷一次陣列,在 key 數量一多時顯然很沒效率。
不會 遍歷原型鏈。
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+}
+
+const keys = Object . keys ( profile ) // ['name', 'age']
+
+keys . includes ( 'name' ) // true
+keys . includes ( 'valueOf' ) // false
+
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+}
+
+const keys = Object . keys ( profile ) // ['name', 'age']
+
+keys . includes ( 'name' ) // true
+keys . includes ( 'valueOf' ) // false
+
`,4),U={id:"!%3D%3D-undefined-%F0%9F%98%90",tabindex:"-1"},V=s("code",{class:""},"!== undefined",-1),W=n(" 😐 "),T={class:"header-anchor",href:"#!%3D%3D-undefined-%F0%9F%98%90"},H=p(`當試圖存取不存在於物件的 key 時,會得到 undefined
。
但當某 key 存在而且值剛好是 undefined
時,那就仍會得到 false
。
會 遍歷原型鏈。
const profile = {
+ name : 'Sean' ,
+ phone : undefined ,
+}
+
+profile . address !== undefined // false
+profile . phone !== undefined // ⚠️ false
+
const profile = {
+ name : 'Sean' ,
+ phone : undefined ,
+}
+
+profile . address !== undefined // false
+profile . phone !== undefined // ⚠️ false
+
`,4),I={id:"!!-%E6%88%96-boolean()-%F0%9F%98%95",tabindex:"-1"},K=s("code",{class:""},"!!",-1),L=n(" 或 "),M=s("code",{class:""},"Boolean()",-1),Q=n(" 😕 "),X={class:"header-anchor",href:"#!!-%E6%88%96-boolean()-%F0%9F%98%95"},Y=p(`簡單暴力的寫法,也就是直接將值轉型成 boolean。但這方法顯然很不可靠,因為只要是 falsy 值,例如 0
、空字串 ''
、 null
等 ,即使 key 存在但依然會得到 false
。
除非你對物件型別有十足的信心,例如在有 TypeScript 的場合,否則不太推薦這寫法。
會 遍歷原型鏈。
const profile = {
+ name : 'Sean' ,
+ balance : 0 ,
+ isDead : false ,
+}
+
+!! profile . name // true
+!! profile . balance // ⚠️ false
+Boolean ( profile . isDead ) // ⚠️ false
+
const profile = {
+ name : 'Sean' ,
+ balance : 0 ,
+ isDead : false ,
+}
+
+!! profile . name // true
+!! profile . balance // ⚠️ false
+Boolean ( profile . isDead ) // ⚠️ false
+
`,4),Z={id:"%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99",tabindex:"-1"},$=n("參考資料 "),ss={class:"header-anchor",href:"#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"},ns=s("ul",null,[s("li",null,[s("a",{href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty"},"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty")]),s("li",null,[s("a",{href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn"},"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn")])],-1),es={title:"檢查物件的 key 是否存在的 N 種方法",date:"2023/07/07",tags:["JavaScript"],original:"https://hackmd.io/@xq/check-if-key-exist"},cs="",ts=e({__name:"check-if-key-exists",setup(as,{expose:o}){return o({frontmatter:{title:"檢查物件的 key 是否存在的 N 種方法",date:"2023/07/07",tags:["JavaScript"],original:"https://hackmd.io/@xq/check-if-key-exist"},excerpt:void 0}),(ls,ps)=>{const a=c("icon-link");return t(),r("div",i,[s("h2",A,[B,d,s("a",D,[l(a)])]),C,s("h2",h,[f,E,s("a",F,[l(a)])]),_,s("h2",k,[u,g,s("a",b,[l(a)])]),O,s("h2",m,[v,w,s("a",j,[l(a)])]),S,s("h2",P,[x,s("a",z,[l(a)])]),s("h3",J,[N,q,s("a",G,[l(a)])]),R,s("h3",U,[V,W,s("a",T,[l(a)])]),H,s("h3",I,[K,L,M,Q,s("a",X,[l(a)])]),Y,s("h2",Z,[$,s("a",ss,[l(a)])]),ns])}}});export{ts as default,cs as excerpt,es as frontmatter};
diff --git a/assets/code-comments-be-like-59cec7d1.jpg b/assets/code-comments-be-like-59cec7d1.jpg
new file mode 100644
index 00000000..7a41730b
Binary files /dev/null and b/assets/code-comments-be-like-59cec7d1.jpg differ
diff --git a/assets/code-review-taxi-number-a7cd2111.js b/assets/code-review-taxi-number-a7cd2111.js
new file mode 100644
index 00000000..7eb26294
--- /dev/null
+++ b/assets/code-review-taxi-number-a7cd2111.js
@@ -0,0 +1,127 @@
+import{d as e,v as c,o as t,a as r,h as s,j as o,k as a,A as l}from"./app-033a95d9.js";const y="/assets/code-comments-be-like-59cec7d1.jpg",B={class:"markdown-body"},A={id:"%E5%89%8D%E8%A8%80",tabindex:"-1"},i=a("前言 "),C={class:"header-anchor",href:"#%E5%89%8D%E8%A8%80"},E=s("p",null,"Code Review 實在是太重要了,我想沒人會否認。",-1),d=s("p",null,"在軟體開發的世界中,需求不斷地變化是家常便飯,程式碼的可讀性和可維護性顯得格外重要。然而 Code Review 要看的多深、多仔細、花多少時間看,想找到適當的平衡點,其中分寸的拿捏確實是個挑戰。",-1),D=s("p",null,[a("但我始終認為有總比沒有好,畢竟 Code Review 是維持程式碼品質最有效的方式。相信大部分開發者都有同感: "),s("strong",null,"「不是新需求難實現,而是舊的扣改不動。」"),a(" 有多少人曾被困在遺留代碼之中,為了加一個新功能,首先得耗費無數的精力,試圖理解那些又長又冗、變數胡亂命名的程式碼。接著才發現函式和函式之間耦合的嚴重,想要重構又因為副作用太多而不敢輕舉妄動。")],-1),h=s("p",null,"因此在這篇文章中我想模擬這樣一個情境:",-1),F=s("blockquote",null,[s("p",null,"如果很嚴格地進行 Code Review,最吹毛求疵的那種,那看起來會像什麼樣子?")],-1),x=s("p",null,"我將以各種壞味道的角度,分析一段出現在真實場景的程式碼,並提供修改建議。同時也分享可套用的對應 ESLint 規則,其官方文件也會解釋這條規則存在的理由是什麼。",-1),u={id:"review-%E5%8E%9F%E5%89%87",tabindex:"-1"},_=a("Review 原則 "),g={class:"header-anchor",href:"#review-%E5%8E%9F%E5%89%87"},m=s("ol",null,[s("li",null,"程式碼應具可讀性"),s("li",null,"以盡量少的程式碼實現相同的功能"),s("li",null,"避免潛在副作用和壞味道")],-1),k={id:"%E9%9C%80%E6%B1%82%E6%8F%8F%E8%BF%B0",tabindex:"-1"},f=a("需求描述 "),v={class:"header-anchor",href:"#%E9%9C%80%E6%B1%82%E6%8F%8F%E8%BF%B0"},b=l('函式接受任意字串,轉換成用來表示「計程車的數量」的數字字串,介於 0
至 30
小於 0
時應總是得到 0
;大於 30
時總是應得到 30
無法用 parseInt
解析的字串一律轉換成 0
轉換後的字串不足兩位數時,左邊補上 0
',1),T={id:"%E5%8E%9F%E7%A8%8B%E5%BC%8F%E7%A2%BC%E7%89%87%E6%AE%B5",tabindex:"-1"},S=a("原程式碼片段 "),w={class:"header-anchor",href:"#%E5%8E%9F%E7%A8%8B%E5%BC%8F%E7%A2%BC%E7%89%87%E6%AE%B5"},R=l(`這 13 行代碼包括註解一字不差地擷自真實專案,其中的人名已替換成假名
// 2021-11-23 Sean : 轉型 > 補0
+const TaxiNumber = ( v ) => {
+ // 正規表示
+ let regex = new RegExp ( / ^ \\d {1,3} $ / );
+ // 叫車最小值、最大值
+ let minTaxi = 0 , maxTaxi = 30 ;
+ // 轉型,利用轉型與輸入特性,處裡數字後字串
+ v = parseInt ( v );
+ // 字串判斷?數值判斷
+ v = regex . test ( v . toString ()) ? v < minTaxi ? minTaxi : v > maxTaxi ? maxTaxi : v : minTaxi ;
+ // 回傳 & 補0
+ return v . toString (). padStart ( 2 , '0' );
+}
+
// 2021-11-23 Sean : 轉型 > 補0
+const TaxiNumber = ( v ) => {
+ // 正規表示
+ let regex = new RegExp ( / ^ \\d {1,3} $ / );
+ // 叫車最小值、最大值
+ let minTaxi = 0 , maxTaxi = 30 ;
+ // 轉型,利用轉型與輸入特性,處裡數字後字串
+ v = parseInt ( v );
+ // 字串判斷?數值判斷
+ v = regex . test ( v . toString ()) ? v < minTaxi ? minTaxi : v > maxTaxi ? maxTaxi : v : minTaxi ;
+ // 回傳 & 補0
+ return v . toString (). padStart ( 2 , '0' );
+}
+
`,2),j={id:"%E5%A3%9E%E5%91%B3%E9%81%93%E4%B8%80%EF%BC%9A%E6%84%8F%E5%9C%96%E4%B8%8D%E6%98%8E%E7%9A%84%E5%87%BD%E5%BC%8F%E5%91%BD%E5%90%8D",tabindex:"-1"},q=a("壞味道一:意圖不明的函式命名 "),N={class:"header-anchor",href:"#%E5%A3%9E%E5%91%B3%E9%81%93%E4%B8%80%EF%BC%9A%E6%84%8F%E5%9C%96%E4%B8%8D%E6%98%8E%E7%9A%84%E5%87%BD%E5%BC%8F%E5%91%BD%E5%90%8D"},I=l(`const TaxiNumber = ( v ) => {
+ ^^^^^^^^^^
+
const TaxiNumber = ( v ) => {
+ ^^^^^^^^^^
+
我首先注意到 TaxiNumber
這個單詞是大駝峰,當讀者單獨看時很容易產生困惑:它是用來展示計程車數字的 React Component 嗎?又或是 class ?但都不是,它只是用來轉換字串的函式。
函式通常是以動詞開頭,而且應命名得清晰明瞭,能夠表達其功能或操作目的。
這類函式建議以 convert
、parse
、format
、normalize
等動詞開頭,例如 convertAToB
,且按照 JavaScript 慣例通常會以小駝峰命名。
💡 套用 ESLint 規則 camelcase
可約束一致變數的命名風格。
`,5),M={id:"%E5%A3%9E%E5%91%B3%E9%81%93%E4%BA%8C%EF%BC%9A%E7%B8%AE%E5%AF%AB%E7%9A%84%E5%8F%83%E6%95%B8",tabindex:"-1"},L=a("壞味道二:縮寫的參數 "),$={class:"header-anchor",href:"#%E5%A3%9E%E5%91%B3%E9%81%93%E4%BA%8C%EF%BC%9A%E7%B8%AE%E5%AF%AB%E7%9A%84%E5%8F%83%E6%95%B8"},J=l(`const TaxiNumber = ( v ) => {
+ ^
+
const TaxiNumber = ( v ) => {
+ ^
+
v
可能是 value
的縮寫,隨個人喜好略縮單詞是個常見的壞味道,通常只有作者本人才知道其背後的含義。但要知道身為團隊的一員,自己不會是唯一維護這段程式碼的人。命名前多花個幾秒仔細思考,將心比心地為下一個接手的人,甚至是一個月後的自己著想,確保它清晰易懂,任何人看了都能夠馬上理解。
建議替換成 countString
,或是任意一具描述性單詞。
💡 套用 ESLint 規則 id-denylist
可以定義「禁詞」,禁止使用這些單詞來命名,例如單字母 a
b
c
,或是毫無意義的 data
、value
。
`,4),V={id:"%E5%A3%9E%E5%91%B3%E9%81%93%E4%B8%89%EF%BC%9A%E5%86%97%E4%BD%99%E7%9A%84%E5%BB%BA%E6%A7%8B%E5%BC%8F",tabindex:"-1"},K=a("壞味道三:冗余的建構式 "),z={class:"header-anchor",href:"#%E5%A3%9E%E5%91%B3%E9%81%93%E4%B8%89%EF%BC%9A%E5%86%97%E4%BD%99%E7%9A%84%E5%BB%BA%E6%A7%8B%E5%BC%8F"},O=l(` let regex = new RegExp ( / ^ \\d {1,3} $ / );
+ ^^^^^^^^^^
+
let regex = new RegExp ( / ^ \\d {1,3} $ / );
+ ^^^^^^^^^^
+
在這個情境並沒打算以 template literals 來動態產生 pattern,例如 new RegExp(\`^{\${a}}$\`)
,所以可直接省略掉 new RegExp()
,直接寫成更簡短的 /^\\d{1,3}$/
即可。
💡 套用 ESLint 規則 prefer-regex-literals
來偏好採用 regular expression literals 寫法。
當能用更簡潔的寫法達成一模一樣的效果時,就沒理由選擇冗長的那個。其他類似的冗余的建構式還有這些:
寫 {}
,而非 new Object()
寫 []
,而非 new Array()
💡 套用 ESLint 規則 no-new-object
和 no-array-constructor
來偏好採用 literal notation 寫法。
`,6),U={id:"%E5%A3%9E%E5%91%B3%E9%81%93%E5%9B%9B%EF%BC%9A%E5%BE%9E%E6%9C%AA%E6%94%B9%E8%AE%8A%E7%9A%84-let",tabindex:"-1"},W=a("壞味道四:從未改變的 "),G=s("code",{class:""},"let",-1),H=a(),P={class:"header-anchor",href:"#%E5%A3%9E%E5%91%B3%E9%81%93%E5%9B%9B%EF%BC%9A%E5%BE%9E%E6%9C%AA%E6%94%B9%E8%AE%8A%E7%9A%84-let"},Q=l(` let minTaxi = 0 , maxTaxi = 30 ;
+ ^^^
+
let minTaxi = 0 , maxTaxi = 30 ;
+ ^^^
+
一個簡單的原則是永遠優先使用 const
來宣告變數,除非真的有需要重新賦值。因此建議改寫成:
/* 常見風格 */
+const min = 0 , max = 30
+
+/* 陣列解構風格 */
+const [ min , max ] = [ 0 , 30 ]
+
/* 常見風格 */
+const min = 0 , max = 30
+
+/* 陣列解構風格 */
+const [ min , max ] = [ 0 , 30 ]
+
順帶一提, [number, number]
的寫法在 TypeScript 中稱作 tuple ,在這隱含一種成雙成對的概念,有助於讀者把它們理解成是「一組範圍」的數字。
💡 套用 ESLint 規則 prefer-const
檢查從未被重新賦值的變數。支援 auto fix,可以自動將 let
替換成 const
。
`,5),X={id:"%E5%A3%9E%E5%91%B3%E9%81%93%E4%BA%94%EF%BC%9A%E5%B0%8D%E5%8F%83%E6%95%B8%E9%87%8D%E6%96%B0%E8%B3%A6%E5%80%BC",tabindex:"-1"},Y=a("壞味道五:對參數重新賦值 "),Z={class:"header-anchor",href:"#%E5%A3%9E%E5%91%B3%E9%81%93%E4%BA%94%EF%BC%9A%E5%B0%8D%E5%8F%83%E6%95%B8%E9%87%8D%E6%96%B0%E8%B3%A6%E5%80%BC"},ss=l(` v = parseInt ( v );
+ ^
+
v = parseInt ( v );
+ ^
+
無論任何語言,複寫函式的參數 從來不是個好主意,可參考一些針對 reassign parameter 的討論 。因為這很容易誤導讀者,原本預期傳入的參數是 string,但突然在某一行後卻硬生生變成了 number。
若專案有導入 TypeScript,某種程度也可以避免出現這種寫法,例如在此例中,因為新值顯然不符合參數原本的型別,compiler 便會直接報錯。
💡 套用 ESLint 規則 no-param-reassign
來防止參數被 reassign。
`,4),as={id:"%E5%A3%9E%E5%91%B3%E9%81%93%E5%85%AD%EF%BC%9A%E5%86%97%E9%95%B7%E7%9A%84%E4%B8%89%E5%85%83%E9%81%8B%E7%AE%97%E5%AD%90",tabindex:"-1"},ns=a("壞味道六:冗長的三元運算子 "),os={class:"header-anchor",href:"#%E5%A3%9E%E5%91%B3%E9%81%93%E5%85%AD%EF%BC%9A%E5%86%97%E9%95%B7%E7%9A%84%E4%B8%89%E5%85%83%E9%81%8B%E7%AE%97%E5%AD%90"},ls=l(` v = regex . test ( v . toString ()) ? v < minTaxi ? minTaxi : v > maxTaxi ? maxTaxi : v : minTaxi ;
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
v = regex . test ( v . toString ()) ? v < minTaxi ? minTaxi : v > maxTaxi ? maxTaxi : v : minTaxi ;
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
巢狀的三元運算子,多個 :
與 ?
交錯,沒有換行與縮排,缺乏可讀性。
建議採用具有更明確的語意的 Math.min()
和 Math.max()
方法改寫,並加上適當的換行和縮排。
v = regex . test ( v . toString ())
+ ? Math . min ( Math . max ( v , minTaxi ), maxTaxi )
+ : minTaxi
+
v = regex . test ( v . toString ())
+ ? Math . min ( Math . max ( v , minTaxi ), maxTaxi )
+ : minTaxi
+
💡 套用 ESLint 規則 no-nested-ternary
來禁止巢狀的三元運算子。
`,5),ps={id:"%E5%A3%9E%E5%91%B3%E9%81%93%E4%B8%83%EF%BC%9A%E6%B7%B1%E5%85%A5%E8%AE%8A%E6%95%B8%E7%9A%84%E5%91%BD%E5%90%8D%E8%97%9D%E8%A1%93",tabindex:"-1"},es=a("壞味道七:深入變數的命名藝術 "),cs={class:"header-anchor",href:"#%E5%A3%9E%E5%91%B3%E9%81%93%E4%B8%83%EF%BC%9A%E6%B7%B1%E5%85%A5%E8%AE%8A%E6%95%B8%E7%9A%84%E5%91%BD%E5%90%8D%E8%97%9D%E8%A1%93"},ts={id:"taxinumber",tabindex:"-1"},rs=s("code",{class:""},"TaxiNumber",-1),ys=a(),Bs={class:"header-anchor",href:"#taxinumber"},As=s("p",null,[s("code",{class:""},"number"),a(" 一詞作為變數有點太籠統了,也沒有好好地起到描述的作用,說到底任何只要是數值想必都會是 number。例如寬度、角度和價格它們都是 number。")],-1),is=s("p",null,[a("要描述這類變數,相信一定找得到比 "),s("code",{class:""},"number"),a(" 還更貼切的單詞。")],-1),Cs=s("p",null,[a("根據需求它是用來表示「計程車的數量」,對於這類"),s("strong",null,"計數/累加"),a("的變數,建議可命名成 "),s("code",{class:""},"count"),a(" 。")],-1),Es={id:"mintaxi",tabindex:"-1"},ds=s("code",{class:""},"minTaxi",-1),Ds=a(),hs={class:"header-anchor",href:"#mintaxi"},Fs=l('第一眼見到 minTaxi
和 maxTaxi
,有點難想像其背後的含義是什麼。讀者可能會把他誤解成車型的大小,或是計程車的載客數。
在這個情況建議單純命名成 min
就行,把 taxi
當作後綴反而不適合,因為實際上要描述的是計程車的「數量」而非「計程車本身」。若想更精準的描述,可以考慮用 minCount
或 minTaxiCount
。
',2),xs={id:"%E5%A3%9E%E5%91%B3%E9%81%93%E5%85%AB%EF%BC%9A%E3%80%8C%2F%2F-%E5%88%B0%E6%AD%A4%E4%B8%80%E9%81%8A%E3%80%8D",tabindex:"-1"},us=a("壞味道八:「"),_s=s("code",{class:""},"// 到此一遊",-1),gs=a("」 "),ms={class:"header-anchor",href:"#%E5%A3%9E%E5%91%B3%E9%81%93%E5%85%AB%EF%BC%9A%E3%80%8C%2F%2F-%E5%88%B0%E6%AD%A4%E4%B8%80%E9%81%8A%E3%80%8D"},ks=l(`// 2021-11-23 Sean : 轉型 > 補0
+
// 2021-11-23 Sean : 轉型 > 補0
+
在未導入版控的遺留專案中,常見在註解裡出現作者的大名。但在現代軟體開發早以 git 進行版控,透過 git blame 就能追溯每一行的修改者是誰、何時修改的、以及對應的 commit message 等。
// 正規表示
+ // 叫車最小值、最大值
+ // 轉型,利用轉型與輸入特性,處裡數字後字串
+ // 字串判斷?數值判斷
+ // 回傳 & 補0
+
// 正規表示
+ // 叫車最小值、最大值
+ // 轉型,利用轉型與輸入特性,處裡數字後字串
+ // 字串判斷?數值判斷
+ // 回傳 & 補0
+
事實上,不只第一行的註解,這整段所有的註解可能都是多餘的,因為程式碼做的事太顯而易見了,註解反而變成閱讀時的雜訊。善用能夠自我描述的變數和函式命名是最好的方法,因為它們本身就能夠清楚地傳達代碼的意圖,不再需要額外的註解。
',5),fs={id:"%E6%9C%80%E7%B5%82%E9%87%8D%E6%A7%8B%E7%B5%90%E6%9E%9C",tabindex:"-1"},vs=a("最終重構結果 "),bs={class:"header-anchor",href:"#%E6%9C%80%E7%B5%82%E9%87%8D%E6%A7%8B%E7%B5%90%E6%9E%9C"},Ts=l(`經過分析不難發現這個函式沒必要用上 RegExp,單純用 parseInt()
就足夠了。因為當傳入不合法的字串時,它就會一律回傳 NaN
,也不會拋出任何錯誤。KISS(Keep It Simple, Stupid)原則 便很好地說明了這個道理,複雜的設計往往容易出錯,增加不必要的複雜度和 debug 的難度,因此應傾向以簡單和直接的方式解決問題,以保持程式碼的清晰度和可讀性。
綜合上述幾點,經過 TypeScript 重構後的程式碼可以長得像這樣:
const parseTaxiCount = ( countString : string ) => {
+ const [ min , max ] = [ 0 , 30 ]
+ const count = parseInt ( countString ) || 0
+ const clampedCount = Math . min ( Math . max ( count , min ), max )
+ const paddedCount = String ( clampedCount ). padStart ( 2 , '0' )
+
+ return paddedCount
+}
+
const parseTaxiCount = ( countString : string ) => {
+ const [ min , max ] = [ 0 , 30 ]
+ const count = parseInt ( countString ) || 0
+ const clampedCount = Math . min ( Math . max ( count , min ), max )
+ const paddedCount = String ( clampedCount ). padStart ( 2 , '0' )
+
+ return paddedCount
+}
+
最後也附上單元測試(Jest):
it ( 'parseTaxiCount()' , () => {
+ expect ( parseTaxiCount ( '' )). toBe ( '00' )
+ expect ( parseTaxiCount ( ' ' )). toBe ( '00' )
+ expect ( parseTaxiCount ( '-1' )). toBe ( '00' )
+ expect ( parseTaxiCount ( '0' )). toBe ( '00' )
+ expect ( parseTaxiCount ( '1' )). toBe ( '01' )
+ expect ( parseTaxiCount ( '01' )). toBe ( '01' )
+ expect ( parseTaxiCount ( '15' )). toBe ( '15' )
+ expect ( parseTaxiCount ( '30' )). toBe ( '30' )
+ expect ( parseTaxiCount ( '31' )). toBe ( '30' )
+ expect ( parseTaxiCount ( '999' )). toBe ( '30' )
+ expect ( parseTaxiCount ( 'abc' )). toBe ( '00' )
+ expect ( parseTaxiCount ( '1a1' )). toBe ( '01' )
+ expect ( parseTaxiCount ( '30aaa' )). toBe ( '30' )
+ expect ( parseTaxiCount ( '030aaa' )). toBe ( '30' )
+})
+
it ( 'parseTaxiCount()' , () => {
+ expect ( parseTaxiCount ( '' )). toBe ( '00' )
+ expect ( parseTaxiCount ( ' ' )). toBe ( '00' )
+ expect ( parseTaxiCount ( '-1' )). toBe ( '00' )
+ expect ( parseTaxiCount ( '0' )). toBe ( '00' )
+ expect ( parseTaxiCount ( '1' )). toBe ( '01' )
+ expect ( parseTaxiCount ( '01' )). toBe ( '01' )
+ expect ( parseTaxiCount ( '15' )). toBe ( '15' )
+ expect ( parseTaxiCount ( '30' )). toBe ( '30' )
+ expect ( parseTaxiCount ( '31' )). toBe ( '30' )
+ expect ( parseTaxiCount ( '999' )). toBe ( '30' )
+ expect ( parseTaxiCount ( 'abc' )). toBe ( '00' )
+ expect ( parseTaxiCount ( '1a1' )). toBe ( '01' )
+ expect ( parseTaxiCount ( '30aaa' )). toBe ( '30' )
+ expect ( parseTaxiCount ( '030aaa' )). toBe ( '30' )
+})
+
`,5),Ss={id:"%E7%B5%90%E8%AA%9E",tabindex:"-1"},ws=a("結語 "),Rs={class:"header-anchor",href:"#%E7%B5%90%E8%AA%9E"},js=s("p",null,"以上分析基於個人開發習慣,提供一些在 Code Review 時可作為參考的觀點,其中很多屬於很主觀的意見,未必是最佳解。因此每個團隊都應該對程式碼風格有所共識,你會發現其實絕大部分都可以交給 ESLint 等自動化工具檢查,將寶貴的 Code Review 時間花在確認是否符合需求、架構的設計是否合理、評估演算法的效能和擴展性等。",-1),qs=s("p",null,"Code Review 的目的不僅僅是確保程式碼的品質和一致性,更重要的是促進團隊之間的合作和溝通。這是一個讓開發者互相學習、分享最佳實踐和技巧,以及達成共識的過程。因此在這個過程中,要盡可能保持開放和建設性的討論,避免淪為單純的批評和指責。",-1),$s={title:"如果太認真去 Code Review 會發生什麼事?",date:"2023/05/31",tags:["JavaScript"]},Js="",Vs=e({__name:"code-review-taxi-number",setup(Ns,{expose:p}){return p({frontmatter:{title:"如果太認真去 Code Review 會發生什麼事?",date:"2023/05/31",tags:["JavaScript"]},excerpt:void 0}),(Is,Ms)=>{const n=c("icon-link");return t(),r("div",B,[s("h2",A,[i,s("a",C,[o(n)])]),E,d,D,h,F,x,s("h3",u,[_,s("a",g,[o(n)])]),m,s("h2",k,[f,s("a",v,[o(n)])]),b,s("h2",T,[S,s("a",w,[o(n)])]),R,s("h2",j,[q,s("a",N,[o(n)])]),I,s("h2",M,[L,s("a",$,[o(n)])]),J,s("h2",V,[K,s("a",z,[o(n)])]),O,s("h2",U,[W,G,H,s("a",P,[o(n)])]),Q,s("h2",X,[Y,s("a",Z,[o(n)])]),ss,s("h2",as,[ns,s("a",os,[o(n)])]),ls,s("h2",ps,[es,s("a",cs,[o(n)])]),s("h3",ts,[rs,ys,s("a",Bs,[o(n)])]),As,is,Cs,s("h3",Es,[ds,Ds,s("a",hs,[o(n)])]),Fs,s("h2",xs,[us,_s,gs,s("a",ms,[o(n)])]),ks,s("h2",fs,[vs,s("a",bs,[o(n)])]),Ts,s("h2",Ss,[ws,s("a",Rs,[o(n)])]),js,qs])}}});export{Vs as default,Js as excerpt,$s as frontmatter};
diff --git a/assets/command-reload-window-955490ff.png b/assets/command-reload-window-955490ff.png
new file mode 100644
index 00000000..2917f6e0
Binary files /dev/null and b/assets/command-reload-window-955490ff.png differ
diff --git a/assets/competition-1f600c5f.png b/assets/competition-1f600c5f.png
new file mode 100644
index 00000000..b802b52a
Binary files /dev/null and b/assets/competition-1f600c5f.png differ
diff --git a/assets/component-naming-87122709.js b/assets/component-naming-87122709.js
new file mode 100644
index 00000000..87d165e6
--- /dev/null
+++ b/assets/component-naming-87122709.js
@@ -0,0 +1,43 @@
+import{d as e,v as t,o as c,a as r,h as s,j as l,k as n,A as o}from"./app-033a95d9.js";const i="/assets/go-to-file-32915399.png",B="/assets/tabs-fc5ce7c7.png",y="/assets/open-editors-844d14f7.png",A={class:"markdown-body"},E={id:"%E5%89%8D%E8%A8%80%EF%BC%9A30-%E7%A7%92%E8%B6%8A%E5%8D%97%E8%AA%9E%E5%B0%8F%E6%95%99%E5%AE%A4-%F0%9F%87%BB%F0%9F%87%B3",tabindex:"-1"},d=n("前言:30 秒越南語小教室 🇻🇳 "),h={class:"header-anchor",href:"#%E5%89%8D%E8%A8%80%EF%BC%9A30-%E7%A7%92%E8%B6%8A%E5%8D%97%E8%AA%9E%E5%B0%8F%E6%95%99%E5%AE%A4-%F0%9F%87%BB%F0%9F%87%B3"},g=o(`越南語是形容詞 後置的語言,恰恰和華語或英語相反。
例如「越南語」這個詞會寫成 「語越南」 :
Tiếng Việt Nam
+ ( 語 ) ( 越南 )
+
Tiếng Việt Nam
+ ( 語 ) ( 越南 )
+
「台灣人」則會寫做 「人台灣」 :
Người Đài Loan
+ ( 人 ) ( 台灣 )
+
Người Đài Loan
+ ( 人 ) ( 台灣 )
+
`,5),C={id:"%E4%BF%AE%E9%A3%BE%E8%AA%9E%E6%93%BA%E5%BE%8C%E9%9D%A2",tabindex:"-1"},D=n("修飾語擺後面 "),_={class:"header-anchor",href:"#%E4%BF%AE%E9%A3%BE%E8%AA%9E%E6%93%BA%E5%BE%8C%E9%9D%A2"},x=o('在 Vue 官方風格指南中,組件命名的概念與越南語文法不謀而合。
若按照英語的文法,人們通常習慣這樣命名組件:
Primary (a.) + Button (n.) = Primary Button Date (n.) + TextField (n.) = Date TextField Confirm (v.) + Dialog (n.) = Confirm Dialog 但如指南所述,它認為應該把🔵主語/主詞 (文中所謂高級別的詞 )擺到前面;🔴修飾性的詞 則是放在結尾。形容詞、動詞或名詞都可算是修飾詞。
因此推薦的命名法如下:
Button (n.) + Primary (a.) = Button Primary TextField (n.) + Date (n.) = TextField Date Dialog (n.) + Confirm (v.) = Dialog Confirm ',6),m={id:"%E5%9C%A8%E7%89%B9%E5%AE%9A%E8%AA%9E%E5%A2%83%E5%85%A7%E7%9A%84%E7%B5%84%E4%BB%B6",tabindex:"-1"},u=n("在特定語境內的組件 "),k={class:"header-anchor",href:"#%E5%9C%A8%E7%89%B9%E5%AE%9A%E8%AA%9E%E5%A2%83%E5%85%A7%E7%9A%84%E7%B5%84%E4%BB%B6"},F=o('對於在🟢特定語境 內才有意義的的組件──也就是在某些個別頁面,或是只有在特定組件中用得到的子組件,指南中也建議以父組件或頁面名當作前綴
例如:
Search (父組件) + TextField + Date = Search TextField Date Search 組件內專用的日期輸入框 Search (父組件) + Button + Submit = Search Button Submit Search 組件內專用的送出按鈕 LandingPage (頁面) + List + News = LandingPage List News 只在 Landing Page 用到的新聞列表 LandingPage (頁面) + List + Features = LandingPage List Features 只在 Landing Page 用到的產品特色列表 ',3),f={id:"%E5%81%9C%E6%AD%A2%E5%B7%A2%E7%8B%80%E8%B3%87%E6%96%99%E5%A4%BE%EF%BC%81",tabindex:"-1"},L=n("停止巢狀資料夾! "),b={class:"header-anchor",href:"#%E5%81%9C%E6%AD%A2%E5%B7%A2%E7%8B%80%E8%B3%87%E6%96%99%E5%A4%BE%EF%BC%81"},v=o(`你可能試過以一層又一層 的目錄來區分組件,像是:
components /
+ |- LandingPage /
+ |- News /
+ |- Item . tsx
+ |- List . tsx
+ |- Title . tsx
+ |- Features . tsx
+ |- Item . tsx
+ |- List . tsx
+ |- Title . tsx
+
components /
+ |- LandingPage /
+ |- News /
+ |- Item . tsx
+ |- List . tsx
+ |- Title . tsx
+ |- Features . tsx
+ |- Item . tsx
+ |- List . tsx
+ |- Title . tsx
+
但你其實可以放心、大膽地將它們都放在同一層 資料夾。
重新調整後的目錄結構如下:
components /
+ |- LandingPageNewsItem . tsx
+ |- LandingPageNewsList . tsx
+ |- LandingPageNewsTitle . tsx
+ |- LandingPageFeaturesItem . tsx
+ |- LandingPageFeaturesList . tsx
+ |- LandingPageFeaturesTitle . tsx
+
components /
+ |- LandingPageNewsItem . tsx
+ |- LandingPageNewsList . tsx
+ |- LandingPageNewsTitle . tsx
+ |- LandingPageFeaturesItem . tsx
+ |- LandingPageFeaturesList . tsx
+ |- LandingPageFeaturesTitle . tsx
+
`,5),P={id:"but-why%3F",tabindex:"-1"},N=n("But Why? "),T={class:"header-anchor",href:"#but-why%3F"},w={id:"%E6%AA%94%E6%A1%88%E7%B8%BD%E7%AE%A1%E7%9A%84%E6%8E%92%E5%BA%8F",tabindex:"-1"},S=n("檔案總管的排序 "),I={class:"header-anchor",href:"#%E6%AA%94%E6%A1%88%E7%B8%BD%E7%AE%A1%E7%9A%84%E6%8E%92%E5%BA%8F"},V=s("p",null,[n("絕大多數 IDE 例如 VSCode,它們的檔案總管都是按照"),s("strong",null,"英文字母"),n("排序的,所以前綴相同的那些文件,都會集中在一塊兒,從命名一眼就能看出哪些組件具有較高關聯性。")],-1),j={id:"%E3%80%8C%E6%BB%BE%E5%8B%95%E3%80%8D%E6%AF%94%E3%80%8C%E5%B1%95%E9%96%8B%E8%B3%87%E6%96%99%E5%A4%BE%E3%80%8D%E6%9B%B4%E7%84%A1%E8%B2%A0%E6%93%94",tabindex:"-1"},z=n("「滾動」比「展開資料夾」更無負擔 "),q={class:"header-anchor",href:"#%E3%80%8C%E6%BB%BE%E5%8B%95%E3%80%8D%E6%AF%94%E3%80%8C%E5%B1%95%E9%96%8B%E8%B3%87%E6%96%99%E5%A4%BE%E3%80%8D%E6%9B%B4%E7%84%A1%E8%B2%A0%E6%93%94"},R=s("p",null,"若要從茫茫組件庫的大海中找尋某個組件,一個一個點擊並展開深層的巢狀資料夾是非常惱人的。更別提有些打開、有些關閉的資料夾相互交錯混雜的畫面,看了更是令人煩躁不安。",-1),G=s("p",null,"相比之下,用滑鼠滾動在同一層目錄瀏覽,顯然更有效率且輕鬆許多。",-1),W={id:"%E7%A7%BB%E5%8B%95%E7%B5%84%E4%BB%B6%E7%9A%84%E5%9B%B0%E9%9B%A3%E5%BA%A6",tabindex:"-1"},Y=n("移動組件的困難度 "),H={class:"header-anchor",href:"#%E7%A7%BB%E5%8B%95%E7%B5%84%E4%BB%B6%E7%9A%84%E5%9B%B0%E9%9B%A3%E5%BA%A6"},J=s("p",null,"對於尚未導入 TypeScript 的專案,想移動組件可能是件大工程。因為 VSCode 可能無法理解組件之間的相依關係,而無法順利地自動進行重構。",-1),K=s("p",null,[n("假設有一個組件 "),s("code",{class:""},"components/searchBar/Input/DateIuput.tsx"),n(",今天我想將它提升至上層的 "),s("code",{class:""},"components/Input/"),n(" 目錄,讓其他更多組件共用。然而我別無選擇,只能在專案中進行的全域搜尋並取代路徑字串,同時也得背負意外替換到其他無辜字串的風險。")],-1),M={id:"%E6%92%9E%E5%90%8D%E7%B5%84%E4%BB%B6%E5%9C%A8%E8%A6%96%E8%A6%BA%E4%B8%8A%E9%9B%A3%E4%BB%A5%E5%AE%9A%E4%BD%8D",tabindex:"-1"},O=n("撞名組件在視覺上難以定位 "),Q={class:"header-anchor",href:"#%E6%92%9E%E5%90%8D%E7%B5%84%E4%BB%B6%E5%9C%A8%E8%A6%96%E8%A6%BA%E4%B8%8A%E9%9B%A3%E4%BB%A5%E5%AE%9A%E4%BD%8D"},U=s("p",null,[n("利用 "),s("code",{class:""},"⌘ + P"),n(" 搜尋眾多同名的組件時,在 VSCode 裡很難快速定位到目標,因為它們乍看之下都一模一樣。")],-1),X=s("p",null,[s("img",{src:i,alt:""})],-1),Z=s("p",null,"同樣地,出現在頁籤中的多個同名檔案也是讓人眼花撩亂。",-1),$=s("p",null,[s("img",{src:B,alt:""})],-1),ss=s("p",null,[s("img",{src:y,alt:""})],-1),ns={id:"%E7%B5%90%E8%AA%9E",tabindex:"-1"},as=n("結語 "),ls={class:"header-anchor",href:"#%E7%B5%90%E8%AA%9E"},os=s("p",null,[n("本文中所提到的命名方式並非強制性的,它只是一種風格,在實務上還是應該考量專案的規模,根據團隊固有的習慣來權衡。不過如果今天你要做的是個人的 side project(例如"),s("a",{href:"https://github.com/ngseke/ngseke.me"},"本專案"),n("),那我會非常推薦你不如大膽地嘗試看看。")],-1),ps={id:"%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99",tabindex:"-1"},es=n("參考資料 "),ts={class:"header-anchor",href:"#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"},cs=s("ul",null,[s("li",null,[s("a",{href:"https://zh.wikipedia.org/zh-tw/%E8%B6%8A%E5%8D%97%E8%AA%9E%E6%96%87%E6%B3%95"},"https://zh.wikipedia.org/zh-tw/越南語文法")]),s("li",null,[s("a",{href:"https://v2.cn.vuejs.org/v2/style-guide"},"https://v2.cn.vuejs.org/v2/style-guide")])],-1),As={title:"試著用有點違反直覺的方式命名組件",date:"2021/11/28",tags:["Vue","React"],original:"https://hackmd.io/@xq/component-naming-reversely"},Es="",ds=e({__name:"component-naming",setup(rs,{expose:p}){return p({frontmatter:{title:"試著用有點違反直覺的方式命名組件",date:"2021/11/28",tags:["Vue","React"],original:"https://hackmd.io/@xq/component-naming-reversely"},excerpt:void 0}),(is,Bs)=>{const a=t("icon-link");return c(),r("div",A,[s("h2",E,[d,s("a",h,[l(a)])]),g,s("h2",C,[D,s("a",_,[l(a)])]),x,s("h2",m,[u,s("a",k,[l(a)])]),F,s("h2",f,[L,s("a",b,[l(a)])]),v,s("h2",P,[N,s("a",T,[l(a)])]),s("h3",w,[S,s("a",I,[l(a)])]),V,s("h3",j,[z,s("a",q,[l(a)])]),R,G,s("h3",W,[Y,s("a",H,[l(a)])]),J,K,s("h3",M,[O,s("a",Q,[l(a)])]),U,X,Z,$,ss,s("h2",ns,[as,s("a",ls,[l(a)])]),os,s("h2",ps,[es,s("a",ts,[l(a)])]),cs])}}});export{ds as default,Es as excerpt,As as frontmatter};
diff --git a/assets/config-21adacbc.png b/assets/config-21adacbc.png
new file mode 100644
index 00000000..51201fca
Binary files /dev/null and b/assets/config-21adacbc.png differ
diff --git a/assets/cover-37fabbf0.png b/assets/cover-37fabbf0.png
new file mode 100644
index 00000000..2482442e
Binary files /dev/null and b/assets/cover-37fabbf0.png differ
diff --git a/assets/cover-76f3ee6d.png b/assets/cover-76f3ee6d.png
new file mode 100644
index 00000000..8fade8b1
Binary files /dev/null and b/assets/cover-76f3ee6d.png differ
diff --git a/assets/cover-background-f6446981.jpg b/assets/cover-background-f6446981.jpg
new file mode 100644
index 00000000..8e3a6ba5
Binary files /dev/null and b/assets/cover-background-f6446981.jpg differ
diff --git a/assets/cover-bdee11bd.png b/assets/cover-bdee11bd.png
new file mode 100644
index 00000000..7bc35c7a
Binary files /dev/null and b/assets/cover-bdee11bd.png differ
diff --git a/assets/cover-c3bb2266.png b/assets/cover-c3bb2266.png
new file mode 100644
index 00000000..8f4cf499
Binary files /dev/null and b/assets/cover-c3bb2266.png differ
diff --git a/assets/cover-c99f79a0.png b/assets/cover-c99f79a0.png
new file mode 100644
index 00000000..7ff7a43a
Binary files /dev/null and b/assets/cover-c99f79a0.png differ
diff --git a/assets/cover-ca6a18cd.png b/assets/cover-ca6a18cd.png
new file mode 100644
index 00000000..ddcdade4
Binary files /dev/null and b/assets/cover-ca6a18cd.png differ
diff --git a/assets/cover-cf0ba68f.png b/assets/cover-cf0ba68f.png
new file mode 100644
index 00000000..b58f98bb
Binary files /dev/null and b/assets/cover-cf0ba68f.png differ
diff --git a/assets/credit-card-calc-28bb5562.js b/assets/credit-card-calc-28bb5562.js
new file mode 100644
index 00000000..dcd28801
--- /dev/null
+++ b/assets/credit-card-calc-28bb5562.js
@@ -0,0 +1 @@
+import{d as a,v as o,o as r,a as s,h as e,j as i,A as n,k as d}from"./app-033a95d9.js";const l="/assets/cover-76f3ee6d.png",p={class:"markdown-body"},u=n('
簡明的信用卡回饋計算機,輸入刷卡金額並選擇帳戶資格即可試算回饋金額。
此專案採用 Vue 3 的各種新特性,如 Composition API、 <script setup>
,並引入 TypeScript 以獲得無縫的開發體驗。
組件完全以 Tailwind CSS 建構,透過 Dark Mode 的 class strategy 和 VueUse 的 useDark()
來輕鬆實現深色模式。
',4),h={id:"demo",tabindex:"-1"},g=d("Demo "),m={class:"header-anchor",href:"#demo"},_=e("p",null,[e("a",{href:"https://ngseke.github.io/sinopac-dual-currency-card-calculator/"},"https://ngseke.github.io/sinopac-dual-currency-card-calculator/")],-1),k=e("p",null,"(註:一代神卡已殞落,2022 年後已不再適用此回饋規則)",-1),b=e("iframe",{src:"https://ghbtns.com/github-btn.html?user=ngseke&repo=/sinopac-dual-currency-card-calculator&type=star&count=false",frameborder:"0",scrolling:"0",width:"150",height:"20"},null,-1),v={title:"Sinopac Dual Currency Card Calc.",briefDescription:"永豐幣倍卡回饋計算機",githubLink:"https://github.com/ngseke/sinopac-dual-currency-card-calculator",demoLink:"https://ngseke.github.io/sinopac-dual-currency-card-calculator/",period:["2021/06","2021/06"],cover:"/img/project-cover/credit-card-calc.png",tags:["Vue","TypeScript","Tailwind CSS","Vite","UI/UX"]},V="",D=a({__name:"credit-card-calc",setup(f,{expose:c}){return c({frontmatter:{title:"Sinopac Dual Currency Card Calc.",briefDescription:"永豐幣倍卡回饋計算機",githubLink:"https://github.com/ngseke/sinopac-dual-currency-card-calculator",demoLink:"https://ngseke.github.io/sinopac-dual-currency-card-calculator/",period:["2021/06","2021/06"],cover:"/img/project-cover/credit-card-calc.png",tags:["Vue","TypeScript","Tailwind CSS","Vite","UI/UX"]},excerpt:void 0}),(y,C)=>{const t=o("icon-link");return r(),s("div",p,[u,e("h2",h,[g,e("a",m,[i(t)])]),_,k,b])}}});export{D as default,V as excerpt,v as frontmatter};
diff --git a/assets/customize-youtube-caption-font-3e0540da.js b/assets/customize-youtube-caption-font-3e0540da.js
new file mode 100644
index 00000000..ee86bde0
--- /dev/null
+++ b/assets/customize-youtube-caption-font-3e0540da.js
@@ -0,0 +1,5 @@
+import{d as a,v as c,o as l,a as p,h as s,j as o,k as n,A as r}from"./app-033a95d9.js";const i="/assets/youtube-cc-menu-f1682830.png",d="/assets/before-485cea7e.jpg",_="/assets/after-8a80ee82.jpg",h={class:"markdown-body"},u={id:"%E8%85%B3%E6%9C%AC%E9%80%A3%E7%B5%90",tabindex:"-1"},f=n("腳本連結 "),m={class:"header-anchor",href:"#%E8%85%B3%E6%9C%AC%E9%80%A3%E7%B5%90"},A=s("p",null,[s("a",{href:"https://gist.github.com/ngseke/8b30050e05703f35c753cd0ac6330028"},"https://gist.github.com/ngseke/8b30050e05703f35c753cd0ac6330028")],-1),y={id:"%E8%AA%AA%E6%98%8E",tabindex:"-1"},E=n("說明 "),g={class:"header-anchor",href:"#%E8%AA%AA%E6%98%8E"},C=r('YouTube CC 字幕雖然有內建設定字體的功能,然而可用的選項並不多,這些選項也都是基於英文字體。
如果你像我一樣對圓體有莫名的愛好,那你一定不能錯過這個腳本。它可以將 YouTube 的字幕替換成任何你想要的字體,前提是你的電腦要先安裝好該字體。
const fontFamily = '"jf-openhuninn-1.1"'
+// ^^^^^^^^^^^^^^^^^
+
const fontFamily = '"jf-openhuninn-1.1"'
+// ^^^^^^^^^^^^^^^^^
+
將第 :13
行替換成想要的字體名稱即可,例如本例用的是開源圓體「jf open 粉圓」 ,注意必須保留裡面的雙引號。
`,5),b={id:"%E7%B5%90%E6%9E%9C",tabindex:"-1"},B=n("結果 "),k={class:"header-anchor",href:"#%E7%B5%90%E6%9E%9C"},x=s("p",null,[s("strong",null,"Before:"),s("br"),s("img",{src:d,alt:""})],-1),D=s("p",null,[s("strong",null,"After:"),s("br"),s("img",{src:_,alt:""})],-1),N={title:"把 YouTube CC 字幕變成粉圓體",date:"2022/08/14",tags:["Tampermonkey"]},V="",Y=a({__name:"customize-youtube-caption-font",setup(j,{expose:t}){return t({frontmatter:{title:"把 YouTube CC 字幕變成粉圓體",date:"2022/08/14",tags:["Tampermonkey"]},excerpt:void 0}),(v,T)=>{const e=c("icon-link");return l(),p("div",h,[s("h2",u,[f,s("a",m,[o(e)])]),A,s("h2",y,[E,s("a",g,[o(e)])]),C,s("h2",b,[B,s("a",k,[o(e)])]),x,D])}}});export{Y as default,V as excerpt,N as frontmatter};
diff --git a/assets/dahezhao-4534134f.jpg b/assets/dahezhao-4534134f.jpg
new file mode 100644
index 00000000..92067d9c
Binary files /dev/null and b/assets/dahezhao-4534134f.jpg differ
diff --git a/assets/dashboard-cc7d8fff.png b/assets/dashboard-cc7d8fff.png
new file mode 100644
index 00000000..0bd9f45d
Binary files /dev/null and b/assets/dashboard-cc7d8fff.png differ
diff --git a/assets/datoutie1-f730def3.png b/assets/datoutie1-f730def3.png
new file mode 100644
index 00000000..05c578e7
Binary files /dev/null and b/assets/datoutie1-f730def3.png differ
diff --git a/assets/datoutie2-b626c0ae.png b/assets/datoutie2-b626c0ae.png
new file mode 100644
index 00000000..c801b0b3
Binary files /dev/null and b/assets/datoutie2-b626c0ae.png differ
diff --git a/assets/datoutie3-99933bc0.png b/assets/datoutie3-99933bc0.png
new file mode 100644
index 00000000..64af4549
Binary files /dev/null and b/assets/datoutie3-99933bc0.png differ
diff --git a/assets/datoutie4-8d7f399d.png b/assets/datoutie4-8d7f399d.png
new file mode 100644
index 00000000..d93ee40f
Binary files /dev/null and b/assets/datoutie4-8d7f399d.png differ
diff --git a/assets/datoutie_fb-b37840ad.png b/assets/datoutie_fb-b37840ad.png
new file mode 100644
index 00000000..aeb8a479
Binary files /dev/null and b/assets/datoutie_fb-b37840ad.png differ
diff --git a/assets/demo-222ae85f.gif b/assets/demo-222ae85f.gif
new file mode 100644
index 00000000..91704d63
Binary files /dev/null and b/assets/demo-222ae85f.gif differ
diff --git a/assets/deployment-f152146e.png b/assets/deployment-f152146e.png
new file mode 100644
index 00000000..7c5fe1cb
Binary files /dev/null and b/assets/deployment-f152146e.png differ
diff --git a/assets/edit-form-5c88d403.png b/assets/edit-form-5c88d403.png
new file mode 100644
index 00000000..1533f381
Binary files /dev/null and b/assets/edit-form-5c88d403.png differ
diff --git a/assets/em-optimization-lab-d89b3c86.js b/assets/em-optimization-lab-d89b3c86.js
new file mode 100644
index 00000000..c588cf1d
--- /dev/null
+++ b/assets/em-optimization-lab-d89b3c86.js
@@ -0,0 +1 @@
+import{d as a,v as i,o as r,a as c,h as t,j as s,k as o,A as p}from"./app-033a95d9.js";const m="/assets/cover-cf0ba68f.png",_="/assets/login-7406d8e1.png",h="/assets/manage-thesis-1289eceb.png",d="/assets/manage-research-b7b5b46f.png",g="/assets/manage-member-9cbfc70b.png",b="/assets/manage-carousel-5d0aae2a.png",l={class:"markdown-body"},u=t("p",null,"這是台北科技大學之電磁最佳化實驗室 (EM Optimization Lab) 的網站,包含 RWD 來增進不同裝置上的瀏覽體驗。",-1),f=t("p",null,"前端主要以 Vue 架構,使用 Sass 和 Pug 搭配 Gulp 自動化流程工具編譯,使用 Firebase 作為資料庫。",-1),E=t("p",null,[t("img",{src:m,alt:"首頁"})],-1),k={id:"%E5%BE%8C%E5%8F%B0%E7%AE%A1%E7%90%86",tabindex:"-1"},w=o("後台管理 "),y={class:"header-anchor",href:"#%E5%BE%8C%E5%8F%B0%E7%AE%A1%E7%90%86"},v=p('網站提供了簡單易用的後台介面,讓管理者能方便快速地更新頁面內容,如修改首頁近期活動 、上傳輪播照片 、 修改實驗室成員 等。
考慮到網站部署空間的限制,因為學校提供的上傳空間只支援靜態網頁,所以資料庫選擇使用 Firebase ,其最大的特色就是只靠靜態的前端網頁就可以操作 database。此外它也可以靠 JS 來上傳檔案到儲存空間,也提供驗證登入的功能,讓擁有權限的使用者才可進入後台編輯。
',3),x={id:"demo",tabindex:"-1"},B=o("Demo "),L={class:"header-anchor",href:"#demo"},V=t("p",null,[t("a",{href:"https://myweb.ntut.edu.tw/~yschen/"},"https://myweb.ntut.edu.tw/~yschen/")],-1),F=t("iframe",{src:"https://ghbtns.com/github-btn.html?user=ngseke&repo=emo&type=star&count=false",frameborder:"0",scrolling:"0",width:"150",height:"20"},null,-1),D={title:"EM Optimization Lab",briefDescription:"《電磁最佳化實驗室》網站",githubLink:"https://github.com/ngseke/emo",demoLink:"https://myweb.ntut.edu.tw/~yschen/",period:["2018/03","2018/10"],cover:"/img/project-cover/emo.png",tags:["Vue","Firebase","gulp.js","UI/UX"]},N="",U=a({__name:"em-optimization-lab",setup(j,{expose:n}){return n({frontmatter:{title:"EM Optimization Lab",briefDescription:"《電磁最佳化實驗室》網站",githubLink:"https://github.com/ngseke/emo",demoLink:"https://myweb.ntut.edu.tw/~yschen/",period:["2018/03","2018/10"],cover:"/img/project-cover/emo.png",tags:["Vue","Firebase","gulp.js","UI/UX"]},excerpt:void 0}),(A,z)=>{const e=i("icon-link");return r(),c("div",l,[u,f,E,t("h2",k,[w,t("a",y,[s(e)])]),v,t("h2",x,[B,t("a",L,[s(e)])]),V,F])}}});export{U as default,N as excerpt,D as frontmatter};
diff --git a/assets/emo-5557c288.js b/assets/emo-5557c288.js
new file mode 100644
index 00000000..f0f7fe5e
--- /dev/null
+++ b/assets/emo-5557c288.js
@@ -0,0 +1 @@
+import{m as o}from"./app-033a95d9.js";const f={};typeof o=="function"&&o(f);export{f as default};
diff --git a/assets/eslint-automatically-fixable-4b63f152.png b/assets/eslint-automatically-fixable-4b63f152.png
new file mode 100644
index 00000000..422e14ef
Binary files /dev/null and b/assets/eslint-automatically-fixable-4b63f152.png differ
diff --git a/assets/facebook-messenger-bf65dd89.png b/assets/facebook-messenger-bf65dd89.png
new file mode 100644
index 00000000..93d4103d
Binary files /dev/null and b/assets/facebook-messenger-bf65dd89.png differ
diff --git a/assets/fb-cover-6fb81625.png b/assets/fb-cover-6fb81625.png
new file mode 100644
index 00000000..696fdf82
Binary files /dev/null and b/assets/fb-cover-6fb81625.png differ
diff --git a/assets/fb_zhuanye-8a3106ac.png b/assets/fb_zhuanye-8a3106ac.png
new file mode 100644
index 00000000..68636f6c
Binary files /dev/null and b/assets/fb_zhuanye-8a3106ac.png differ
diff --git a/assets/fe-e736b23f.js b/assets/fe-e736b23f.js
new file mode 100644
index 00000000..c22f85c7
--- /dev/null
+++ b/assets/fe-e736b23f.js
@@ -0,0 +1 @@
+import{d as t,g as a,B as s,C as r,o as c,a as u,m as e}from"./app-033a95d9.js";const p=t({__name:"fe",setup(l){const{isDownplayed:o}=a();o.value=!0;const n=s();return r(()=>{n.replace({name:"index"})}),(f,i)=>(c(),u("div"))}});typeof e=="function"&&e(p);export{p as default};
diff --git a/assets/flag-5557c288.js b/assets/flag-5557c288.js
new file mode 100644
index 00000000..f0f7fe5e
--- /dev/null
+++ b/assets/flag-5557c288.js
@@ -0,0 +1 @@
+import{m as o}from"./app-033a95d9.js";const f={};typeof o=="function"&&o(f);export{f as default};
diff --git a/assets/forms-d0d210e4.png b/assets/forms-d0d210e4.png
new file mode 100644
index 00000000..9b210bc5
Binary files /dev/null and b/assets/forms-d0d210e4.png differ
diff --git a/assets/game1-d689a75e.png b/assets/game1-d689a75e.png
new file mode 100644
index 00000000..e0562c65
Binary files /dev/null and b/assets/game1-d689a75e.png differ
diff --git a/assets/game2-ec9b0a6e.png b/assets/game2-ec9b0a6e.png
new file mode 100644
index 00000000..465bc8bd
Binary files /dev/null and b/assets/game2-ec9b0a6e.png differ
diff --git a/assets/game3-ae563b09.png b/assets/game3-ae563b09.png
new file mode 100644
index 00000000..1f2ded0a
Binary files /dev/null and b/assets/game3-ae563b09.png differ
diff --git a/assets/game4-ad0db896.png b/assets/game4-ad0db896.png
new file mode 100644
index 00000000..f47206fa
Binary files /dev/null and b/assets/game4-ad0db896.png differ
diff --git a/assets/go-to-file-32915399.png b/assets/go-to-file-32915399.png
new file mode 100644
index 00000000..edf83601
Binary files /dev/null and b/assets/go-to-file-32915399.png differ
diff --git a/assets/gomoku-e312c04b.js b/assets/gomoku-e312c04b.js
new file mode 100644
index 00000000..d4716c8b
--- /dev/null
+++ b/assets/gomoku-e312c04b.js
@@ -0,0 +1 @@
+import{d as s,v as i,o as r,a as c,h as e,j as n,A as g,k as m}from"./app-033a95d9.js";const p="data:image/png;base64,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",a="/assets/register-0126b216.png",u="/assets/room-f8da92ed.png",d="/assets/cover-bdee11bd.png",k="/assets/mobile-bc89f109.png",h={class:"markdown-body"},E=g('
Gomoku: 五子棋對戰 是簡約的五子棋遊戲,是與小夥伴在悠閒午後消磨時間的良伴。
',5),U={id:"demo",tabindex:"-1"},I=m("Demo "),A={class:"header-anchor",href:"#demo"},b=e("p",null,[e("a",{href:"https://gomoku.ngseke.me"},"https://gomoku.ngseke.me")],-1),f=e("iframe",{src:"https://ghbtns.com/github-btn.html?user=ngseke&repo=gomoku&type=star&count=false",frameborder:"0",scrolling:"0",width:"150",height:"20"},null,-1),S={title:"Gomoku",briefDescription:"線上五子棋對戰",githubLink:"https://github.com/ngseke/gomoku",demoLink:"https://gomoku.ngseke.me",period:["2018/10","2018/11"],cover:"/img/project-cover/gomoku.png",tags:["Vue","Firebase","UI/UX"]},R="",X=s({__name:"gomoku",setup(l,{expose:o}){return o({frontmatter:{title:"Gomoku",briefDescription:"線上五子棋對戰",githubLink:"https://github.com/ngseke/gomoku",demoLink:"https://gomoku.ngseke.me",period:["2018/10","2018/11"],cover:"/img/project-cover/gomoku.png",tags:["Vue","Firebase","UI/UX"]},excerpt:void 0}),(F,Q)=>{const t=i("icon-link");return r(),c("div",h,[E,e("h2",U,[I,e("a",A,[n(t)])]),b,f])}}});export{X as default,R as excerpt,S as frontmatter};
diff --git a/assets/hengfu-324e5744.png b/assets/hengfu-324e5744.png
new file mode 100644
index 00000000..c565f2cf
Binary files /dev/null and b/assets/hengfu-324e5744.png differ
diff --git a/assets/hover-type-d5f8cd06.png b/assets/hover-type-d5f8cd06.png
new file mode 100644
index 00000000..e8cbce76
Binary files /dev/null and b/assets/hover-type-d5f8cd06.png differ
diff --git a/assets/icon-3f390287.png b/assets/icon-3f390287.png
new file mode 100644
index 00000000..c4b1c866
Binary files /dev/null and b/assets/icon-3f390287.png differ
diff --git a/assets/index-4cd242c9.js b/assets/index-4cd242c9.js
new file mode 100644
index 00000000..018bd6d0
--- /dev/null
+++ b/assets/index-4cd242c9.js
@@ -0,0 +1 @@
+import{d as r,a3 as m,o as t,a as s,h as e,F as _,i as p,t as d,j as u,u as x,a4 as f,m as a}from"./app-033a95d9.js";const g={class:"container px-4 pt-16"},h={class:"mx-auto max-w-3xl py-8 sm:py-16"},k=e("h1",{class:"mb-12 text-4xl font-semibold dark:text-ngsek md:text-5xl"}," ngseke's Blog ",-1),y={class:"flex items-center text-3xl font-medium opacity-70 lg:absolute lg:-left-6 lg:-translate-x-full"},b=r({__name:"index",setup(B){const{postGroups:o}=m();return(v,F)=>{const n=f;return t(),s("div",g,[e("div",h,[k,(t(!0),s(_,null,p(x(o),({name:l,posts:c},i)=>(t(),s("section",{key:i,class:"relative mb-16 space-y-8"},[e("h2",y,d(l),1),u(n,{list:c},null,8,["list"])]))),128))])])}}});typeof a=="function"&&a(b);export{b as default};
diff --git a/assets/index-95e78139.js b/assets/index-95e78139.js
new file mode 100644
index 00000000..f0f7fe5e
--- /dev/null
+++ b/assets/index-95e78139.js
@@ -0,0 +1 @@
+import{m as o}from"./app-033a95d9.js";const f={};typeof o=="function"&&o(f);export{f as default};
diff --git a/assets/index-f96aa53f.css b/assets/index-f96aa53f.css
new file mode 100644
index 00000000..7102aa72
--- /dev/null
+++ b/assets/index-f96aa53f.css
@@ -0,0 +1 @@
+#nprogress{pointer-events:none}#nprogress .bar{background:#29d;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}#nprogress .peg{display:block;position:absolute;right:0;width:100px;height:100%;box-shadow:0 0 10px #29d,0 0 5px #29d;opacity:1;transform:rotate(3deg) translateY(-4px)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}#nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:solid 2px transparent;border-top-color:#29d;border-left-color:#29d;border-radius:50%;-webkit-animation:nprogress-spinner .4s linear infinite;animation:nprogress-spinner .4s linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .spinner,.nprogress-custom-parent #nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0)}to{transform:rotate(360deg)}}*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Barlow Semi Condensed,Noto Sans TC,sans-serif;font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:Fira Code,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}body{touch-action:manipulation;--tw-text-opacity: 1;color:rgb(23 23 23 / var(--tw-text-opacity))}:is(.dark body){--tw-bg-opacity: 1;background-color:rgb(23 23 23 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(245 245 245 / var(--tw-text-opacity))}code,kbd,samp,pre{font-family:Fira Code,monospace}p{letter-spacing:.2px}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::-webkit-backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%;margin-right:auto;margin-left:auto}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.link-effect{position:relative}.link-effect:after{pointer-events:none;position:absolute;z-index:-1;--tw-scale-x: .75;--tw-scale-y: .75;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));opacity:0;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);content:var(--tw-content);transition-duration:.2s}@media (hover: hover) and (pointer: fine){.link-effect:hover:after{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));content:var(--tw-content);opacity:1}}.link-effect:active{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.link-effect:active:after{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));content:var(--tw-content);opacity:1}.pointer-events-none{pointer-events:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.inset-4{top:1rem;right:1rem;bottom:1rem;left:1rem}.left-0{left:0}.right-0{right:0}.top-0{top:0}.top-1\/2{top:50%}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-50{z-index:50}.col-auto{grid-column:auto}.\!m-0{margin:0!important}.-mx-4{margin-left:-1rem;margin-right:-1rem}.mx-0{margin-left:0;margin-right:0}.mx-auto{margin-left:auto;margin-right:auto}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.\!mb-2{margin-bottom:.5rem!important}.\!mt-0{margin-top:0!important}.\!mt-10{margin-top:2.5rem!important}.-mt-2{margin-top:-.5rem}.mb-12{margin-bottom:3rem}.mb-16{margin-bottom:4rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-3{margin-right:.75rem}.mr-5{margin-right:1.25rem}.mr-6{margin-right:1.5rem}.mt-12{margin-top:3rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-14{height:3.5rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-\[12\.1rem\]{height:12.1rem}.h-\[5\.5rem\]{height:5.5rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-\[600px\]{max-height:600px}.max-h-full{max-height:100%}.min-h-\[8rem\]{min-height:8rem}.min-h-screen{min-height:100vh}.w-1\/12{width:8.333333%}.w-1\/2{width:50%}.w-1\/3{width:33.333333%}.w-1\/4{width:25%}.w-10{width:2.5rem}.w-3\/5{width:60%}.w-4\/5{width:80%}.w-5{width:1.25rem}.w-8{width:2rem}.w-\[17\.6rem\]{width:17.6rem}.w-\[5rem\]{width:5rem}.w-\[8rem\]{width:8rem}.w-full{width:100%}.min-w-\[1\.5rem\]{min-width:1.5rem}.min-w-\[50\%\]{min-width:50%}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-\[1200px\]{max-width:1200px}.max-w-\[250px\]{max-width:250px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-none{flex:none}.grow{flex-grow:1}.origin-left{transform-origin:left}.origin-top-left{transform-origin:top left}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-45{--tw-rotate: -45deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-45{--tw-rotate: 45deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-\[-4deg\]{--tw-rotate: -4deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-0{--tw-scale-x: 0;--tw-scale-y: 0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-75{--tw-scale-x: .75;--tw-scale-y: .75;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-90{--tw-scale-x: .9;--tw-scale-y: .9;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-95{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.list-outside{list-style-position:outside}.\!list-none{list-style-type:none!important}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-3{gap:.75rem}.gap-8{gap:2rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.25rem * var(--tw-space-x-reverse));margin-left:calc(.25rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.75rem * var(--tw-space-x-reverse));margin-left:calc(.75rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-6>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1.5rem * var(--tw-space-x-reverse));margin-left:calc(1.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-14>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(3.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(3.5rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-\[1rem\]{border-bottom-width:1rem}.border-l-4{border-left-width:4px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-dotted{border-style:dotted}.border-black-200{--tw-border-opacity: 1;border-color:rgb(229 229 229 / var(--tw-border-opacity))}.border-black-300{--tw-border-opacity: 1;border-color:rgb(212 212 212 / var(--tw-border-opacity))}.border-black-500\/50{border-color:#73737380}.border-black-800{--tw-border-opacity: 1;border-color:rgb(38 38 38 / var(--tw-border-opacity))}.border-current{border-color:currentColor}.border-ngsek{--tw-border-opacity: 1;border-color:rgb(255 208 25 / var(--tw-border-opacity))}.border-transparent{border-color:transparent}.bg-amber-500{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity))}.bg-black-100{--tw-bg-opacity: 1;background-color:rgb(245 245 245 / var(--tw-bg-opacity))}.bg-black-200{--tw-bg-opacity: 1;background-color:rgb(229 229 229 / var(--tw-bg-opacity))}.bg-black-400\/10{background-color:#a3a3a31a}.bg-black-400\/20{background-color:#a3a3a333}.bg-black-900{--tw-bg-opacity: 1;background-color:rgb(23 23 23 / var(--tw-bg-opacity))}.bg-black-900\/20{background-color:#17171733}.bg-lime-500{--tw-bg-opacity: 1;background-color:rgb(132 204 22 / var(--tw-bg-opacity))}.bg-orange-500{--tw-bg-opacity: 1;background-color:rgb(249 115 22 / var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.bg-sky-500{--tw-bg-opacity: 1;background-color:rgb(14 165 233 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-white\/95{background-color:#fffffff2}.bg-cover{background-size:cover}.bg-center{background-position:center}.object-cover{-o-object-fit:cover;object-fit:cover}.\!p-0{padding:0!important}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-20{padding-left:5rem;padding-right:5rem}.px-4{padding-left:1rem;padding-right:1rem}.px-8{padding-left:2rem;padding-right:2rem}.px-\[\.4rem\]{padding-left:.4rem;padding-right:.4rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-14{padding-bottom:3.5rem}.pb-4{padding-bottom:1rem}.pb-8{padding-bottom:2rem}.pl-1{padding-left:.25rem}.pl-5{padding-left:1.25rem}.pt-10{padding-top:2.5rem}.pt-16{padding-top:4rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.text-center{text-align:center}.align-middle{vertical-align:middle}.font-mono{font-family:Fira Code,monospace}.font-pacifico{font-family:Pacifico,cursive}.\!text-lg{font-size:1.125rem!important;line-height:1.75rem!important}.\!text-sm{font-size:.875rem!important;line-height:1.25rem!important}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-6xl{font-size:3.75rem;line-height:1}.text-8xl{font-size:6rem;line-height:1}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.\!font-medium{font-weight:500!important}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.not-italic{font-style:normal}.\!leading-4{line-height:1rem!important}.\!leading-5{line-height:1.25rem!important}.leading-7{line-height:1.75rem}.leading-loose{line-height:2}.leading-none{line-height:1}.leading-tight{line-height:1.25}.tracking-\[\.2px\]{letter-spacing:.2px}.tracking-\[1px\]{letter-spacing:1px}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-black-400{--tw-text-opacity: 1;color:rgb(163 163 163 / var(--tw-text-opacity))}.text-black-700{--tw-text-opacity: 1;color:rgb(64 64 64 / var(--tw-text-opacity))}.text-black-900\/80{color:#171717cc}.text-ngsek{--tw-text-opacity: 1;color:rgb(255 208 25 / var(--tw-text-opacity))}.text-rose-600{--tw-text-opacity: 1;color:rgb(225 29 72 / var(--tw-text-opacity))}.text-sky-600{--tw-text-opacity: 1;color:rgb(2 132 199 / var(--tw-text-opacity))}.text-teal-600{--tw-text-opacity: 1;color:rgb(13 148 136 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-white\/80{color:#fffc}.underline{text-decoration-line:underline}.decoration-dotted{text-decoration-style:dotted}.opacity-0{opacity:0}.opacity-40{opacity:.4}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-md{--tw-backdrop-blur: blur(12px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.delay-200{transition-delay:.2s}.duration-100{transition-duration:.1s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.duration-700{transition-duration:.7s}.duration-75{transition-duration:75ms}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}#nprogress .bar{--tw-bg-opacity: 1;background-color:rgb(255 208 25 / var(--tw-bg-opacity))}#nprogress .peg{box-shadow:0 0 20px #ffd019,0 0 10px #ffd019,0 0 5px #ffd019}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:left-0:before{content:var(--tw-content);left:0}.before\:top-0:before{content:var(--tw-content);top:0}.before\:-z-10:before{content:var(--tw-content);z-index:-10}.before\:-mt-20:before{content:var(--tw-content);margin-top:-5rem}.before\:mr-1:before{content:var(--tw-content);margin-right:.25rem}.before\:block:before{content:var(--tw-content);display:block}.before\:inline-block:before{content:var(--tw-content);display:inline-block}.before\:h-20:before{content:var(--tw-content);height:5rem}.before\:h-full:before{content:var(--tw-content);height:100%}.before\:w-full:before{content:var(--tw-content);width:100%}.before\:min-w-\[\.5rem\]:before{content:var(--tw-content);min-width:.5rem}.before\:origin-right:before{content:var(--tw-content);transform-origin:right}.before\:rounded-xl:before{content:var(--tw-content);border-radius:.75rem}.before\:bg-black-100:before{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(245 245 245 / var(--tw-bg-opacity))}.before\:text-center:before{content:var(--tw-content);text-align:center}.before\:opacity-50:before{content:var(--tw-content);opacity:.5}.before\:content-\[\"•\"\]:before{--tw-content: "•";content:var(--tw-content)}.before\:content-\[\"▸\"\]:before{--tw-content: "▸";content:var(--tw-content)}.before\:content-\[\'\#\'\]:before{--tw-content: "#";content:var(--tw-content)}.before\:content-\[\'\'\]:before{--tw-content: "";content:var(--tw-content)}.before\:content-\[\'▸\'\]:before{--tw-content: "▸";content:var(--tw-content)}.after\:pointer-events-none:after{content:var(--tw-content);pointer-events:none}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:-inset-3:after{content:var(--tw-content);top:-.75rem;right:-.75rem;bottom:-.75rem;left:-.75rem}.after\:inset-\[-\.3rem\]:after{content:var(--tw-content);top:-.3rem;right:-.3rem;bottom:-.3rem;left:-.3rem}.after\:-inset-x-2:after{content:var(--tw-content);left:-.5rem;right:-.5rem}.after\:-inset-x-3:after{content:var(--tw-content);left:-.75rem;right:-.75rem}.after\:-inset-y-1:after{content:var(--tw-content);top:-.25rem;bottom:-.25rem}.after\:-inset-y-2:after{content:var(--tw-content);top:-.5rem;bottom:-.5rem}.after\:inset-x-\[-1px\]:after{content:var(--tw-content);left:-1px;right:-1px}.after\:inset-x-\[-8px\]:after{content:var(--tw-content);left:-8px;right:-8px}.after\:inset-y-\[-6px\]:after{content:var(--tw-content);top:-6px;bottom:-6px}.after\:-bottom-8:after{content:var(--tw-content);bottom:-2rem}.after\:bottom-\[10\%\]:after{content:var(--tw-content);bottom:10%}.after\:left-0:after{content:var(--tw-content);left:0}.after\:top-0:after{content:var(--tw-content);top:0}.after\:-z-10:after{content:var(--tw-content);z-index:-10}.after\:-z-20:after{content:var(--tw-content);z-index:-20}.after\:block:after{content:var(--tw-content);display:block}.after\:h-8:after{content:var(--tw-content);height:2rem}.after\:h-\[20\%\]:after{content:var(--tw-content);height:20%}.after\:h-full:after{content:var(--tw-content);height:100%}.after\:w-full:after{content:var(--tw-content);width:100%}.after\:scale-\[97\%\]:after{content:var(--tw-content);--tw-scale-x: 97%;--tw-scale-y: 97%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.after\:overflow-hidden:after{content:var(--tw-content);overflow:hidden}.after\:rounded-full:after{content:var(--tw-content);border-radius:9999px}.after\:rounded-lg:after{content:var(--tw-content);border-radius:.5rem}.after\:rounded-xl:after{content:var(--tw-content);border-radius:.75rem}.after\:bg-black-400\/10:after{content:var(--tw-content);background-color:#a3a3a31a}.after\:bg-ngsek\/60:after{content:var(--tw-content);background-color:#ffd01999}.after\:bg-gradient-to-b:after{content:var(--tw-content);background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.after\:from-white:after{content:var(--tw-content);--tw-gradient-from: #fff var(--tw-gradient-from-position);--tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.after\:to-\[transparent_70\%\]:after{content:var(--tw-content);--tw-gradient-to: transparent 70% var(--tw-gradient-to-position)}.after\:bg-cover:after{content:var(--tw-content);background-size:cover}.after\:bg-center:after{content:var(--tw-content);background-position:center}.after\:pt-\[56\.25\%\]:after{content:var(--tw-content);padding-top:56.25%}.after\:text-transparent:after{content:var(--tw-content);color:transparent}.after\:opacity-0:after{content:var(--tw-content);opacity:0}.after\:opacity-40:after{content:var(--tw-content);opacity:.4}.after\:blur-lg:after{content:var(--tw-content);--tw-blur: blur(16px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.after\:transition-all:after{content:var(--tw-content);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.after\:transition-opacity:after{content:var(--tw-content);transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.after\:duration-200:after{content:var(--tw-content);transition-duration:.2s}.after\:duration-500:after{content:var(--tw-content);transition-duration:.5s}.after\:content-\[\'ngseke\'\]:after{--tw-content: "ngseke";content:var(--tw-content)}.after\:\[background-image\:var\(--img\)\]:after{content:var(--tw-content);background-image:var(--img)}.after\:\[will-change\:filter\]:after{content:var(--tw-content);will-change:filter}@media (hover: hover) and (pointer: fine){.hover\:bg-black-400\/20:hover{background-color:#a3a3a333}.hover\:after\:scale-100:hover:after{content:var(--tw-content);--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:after\:opacity-100:hover:after{content:var(--tw-content);opacity:1}}:is(.dark .dark\:border-black-700){--tw-border-opacity: 1;border-color:rgb(64 64 64 / var(--tw-border-opacity))}:is(.dark .dark\:border-black-800){--tw-border-opacity: 1;border-color:rgb(38 38 38 / var(--tw-border-opacity))}:is(.dark .dark\:bg-black-800){--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-black-900){--tw-bg-opacity: 1;background-color:rgb(23 23 23 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-black-900\/95){background-color:#171717f2}:is(.dark .dark\:text-black-300){--tw-text-opacity: 1;color:rgb(212 212 212 / var(--tw-text-opacity))}:is(.dark .dark\:text-black-400){--tw-text-opacity: 1;color:rgb(163 163 163 / var(--tw-text-opacity))}:is(.dark .dark\:text-ngsek){--tw-text-opacity: 1;color:rgb(255 208 25 / var(--tw-text-opacity))}:is(.dark .dark\:text-white\/80){color:#fffc}:is(.dark .dark\:before\:bg-black-800):before{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity))}:is(.dark .dark\:after\:bg-ngsek\/30):after{content:var(--tw-content);background-color:#ffd0194d}:is(.dark .dark\:after\:from-black-900):after{content:var(--tw-content);--tw-gradient-from: #171717 var(--tw-gradient-from-position);--tw-gradient-to: rgb(23 23 23 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}@media print{.print\:hidden{display:none}}@media (min-width: 640px){.sm\:static{position:static}.sm\:right-auto{right:auto}.sm\:top-auto{top:auto}.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:h-16{height:4rem}.sm\:\!w-0{width:0px!important}.sm\:w-24{width:6rem}.sm\:w-auto{width:auto}.sm\:flex-none{flex:none}.sm\:scale-100{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:transform-none{transform:none}.sm\:justify-start{justify-content:flex-start}.sm\:py-16{padding-top:4rem;padding-bottom:4rem}.sm\:text-6xl{font-size:3.75rem;line-height:1}}@media (min-width: 768px){.md\:w-1\/2{width:50%}.md\:flex-1{flex:1 1 0%}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:pt-16{padding-top:4rem}.md\:text-4xl{font-size:2.25rem;line-height:2.5rem}.md\:text-5xl{font-size:3rem;line-height:1}.md\:text-7xl{font-size:4.5rem;line-height:1}.md\:leading-tight{line-height:1.25}}@media (min-width: 1024px){.lg\:absolute{position:absolute}.lg\:-left-6{left:-1.5rem}.lg\:w-full{width:100%}.lg\:flex-none{flex:none}.lg\:-translate-x-full{--tw-translate-x: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}}.neon[data-v-b1cdf682]{color:#fff;text-shadow:0 0 6px rgba(255,255,255,.92),0 0 30px rgba(255,255,255,.34),0 0 12px rgba(255,208,25,.52),0 0 21px rgba(255,208,25,.5),0 0 34px rgba(255,208,25,.7),0 0 54px rgba(255,208,25,.5)}.blink[data-v-b1cdf682]:after{text-shadow:0 0 54px rgba(255,208,25,.3);-webkit-animation:blink-b1cdf682 .08s ease-in-out infinite alternate;animation:blink-b1cdf682 .08s ease-in-out infinite alternate}@-webkit-keyframes blink-b1cdf682{0%{opacity:0%}}@keyframes blink-b1cdf682{0%{opacity:0%}}
diff --git a/assets/intellisense-e7791f21.png b/assets/intellisense-e7791f21.png
new file mode 100644
index 00000000..b72d3844
Binary files /dev/null and b/assets/intellisense-e7791f21.png differ
diff --git a/assets/interface3-2a7d9ac4.png b/assets/interface3-2a7d9ac4.png
new file mode 100644
index 00000000..62f6356b
Binary files /dev/null and b/assets/interface3-2a7d9ac4.png differ
diff --git a/assets/interface4-6491aa95.png b/assets/interface4-6491aa95.png
new file mode 100644
index 00000000..7dbf73f0
Binary files /dev/null and b/assets/interface4-6491aa95.png differ
diff --git a/assets/interface5-fd8930fa.png b/assets/interface5-fd8930fa.png
new file mode 100644
index 00000000..377e7c6d
Binary files /dev/null and b/assets/interface5-fd8930fa.png differ
diff --git a/assets/iphone-price-c267c7d7.js b/assets/iphone-price-c267c7d7.js
new file mode 100644
index 00000000..79253dd5
--- /dev/null
+++ b/assets/iphone-price-c267c7d7.js
@@ -0,0 +1 @@
+import{d as n,v as c,o as r,a as p,h as e,j as o,k as s}from"./app-033a95d9.js";const a="/assets/cover-37fabbf0.png",h="/assets/1-408d5807.png",_="/assets/2-c1925776.png",d="/assets/3-c180c5f6.png",m={class:"markdown-body"},g=e("p",null,[e("img",{src:a,alt:""})],-1),l={id:"%E7%B0%A1%E4%BB%8B",tabindex:"-1"},k=s("簡介 "),f={class:"header-anchor",href:"#%E7%B0%A1%E4%BB%8B"},b=e("p",null,[e("img",{src:h,alt:""}),e("br"),e("img",{src:_,alt:""}),e("br"),e("img",{src:d,alt:""})],-1),u={id:"demo",tabindex:"-1"},B=s("Demo "),v={class:"header-anchor",href:"#demo"},x=e("p",null,[e("a",{href:"https://iphone-price.ngseke.me/"},"https://iphone-price.ngseke.me/")],-1),T={title:"iPhone Price",briefDescription:"台灣 iPhone 價格歷史趨勢",githubLink:"https://github.com/ngseke/iphone-price",demoLink:"https://iphone-price.ngseke.me/",period:["2023/10","2023/10"],cover:"/img/project-cover/iphone-price.png",tags:["Vue","TypeScript","Tailwind","daisyUI","ECharts","UI/UX"]},V="",C=n({__name:"iphone-price",setup(E,{expose:i}){return i({frontmatter:{title:"iPhone Price",briefDescription:"台灣 iPhone 價格歷史趨勢",githubLink:"https://github.com/ngseke/iphone-price",demoLink:"https://iphone-price.ngseke.me/",period:["2023/10","2023/10"],cover:"/img/project-cover/iphone-price.png",tags:["Vue","TypeScript","Tailwind","daisyUI","ECharts","UI/UX"]},excerpt:void 0}),(P,U)=>{const t=c("icon-link");return r(),p("div",m,[g,e("h2",l,[k,e("a",f,[o(t)])]),b,e("h2",u,[B,e("a",v,[o(t)])]),x])}}});export{C as default,V as excerpt,T as frontmatter};
diff --git a/assets/koasu-5245e6bf.js b/assets/koasu-5245e6bf.js
new file mode 100644
index 00000000..15164851
--- /dev/null
+++ b/assets/koasu-5245e6bf.js
@@ -0,0 +1 @@
+import{d as s,v as a,o as i,a as r,h as t,j as n,A as h,k as c}from"./app-033a95d9.js";const p="/assets/cover-c99f79a0.png",u={class:"markdown-body"},g=h('KOASÛ 也就是台語「歌詞koa-sû 」的白話字。
本網誌基於 Hexo 架設,主要受到台客鬼 和歌詞正字 的啟發,補全它們未收錄的台語歌詞,特別是某些非主流歌曲,例如來去高雄 。本人將這些歌詞翻譯成符合教育部所規範的台文正字,並以白話字表記發音,也希望藉此過程來提高自己對白話字的掌握度。
台文漢字的選用與拼法主要參考臺灣閩南語常用詞辭典 、萌典 和iTaigi 愛台語 。
',4),k={id:"demo",tabindex:"-1"},d=c("Demo "),m={class:"header-anchor",href:"#demo"},_=t("p",null,[t("a",{href:"https://ngseke.github.io/koasu/"},"https://ngseke.github.io/koasu/")],-1),l=t("iframe",{src:"https://ghbtns.com/github-btn.html?user=ngseke&repo=koasu&type=star&count=false",frameborder:"0",scrolling:"0",width:"150",height:"20"},null,-1),x={title:"Koasu",briefDescription:"KOASÛ 白話字台語歌詞網誌",githubLink:"https://github.com/ngseke/koasu",demoLink:"https://ngseke.github.io/koasu/",period:"2021/08",cover:"/img/project-cover/koasu.png",tags:["Hexo"]},K="",y=s({__name:"koasu",setup(b,{expose:e}){return e({frontmatter:{title:"Koasu",briefDescription:"KOASÛ 白話字台語歌詞網誌",githubLink:"https://github.com/ngseke/koasu",demoLink:"https://ngseke.github.io/koasu/",period:"2021/08",cover:"/img/project-cover/koasu.png",tags:["Hexo"]},excerpt:void 0}),(f,w)=>{const o=a("icon-link");return i(),r("div",u,[g,t("h2",k,[d,t("a",m,[n(o)])]),_,l])}}});export{y as default,K as excerpt,x as frontmatter};
diff --git a/assets/leetcode-night-b75823ff.js b/assets/leetcode-night-b75823ff.js
new file mode 100644
index 00000000..768a4b04
--- /dev/null
+++ b/assets/leetcode-night-b75823ff.js
@@ -0,0 +1 @@
+import{d as c,v as n,o as l,a,h as e,j as s,k as t,A as r}from"./app-033a95d9.js";const d="/assets/banner-63e3ec60.png",h="/assets/store-f420ca97.png",p="/assets/2-8417bfde.png",g="/assets/1-d295ad8e.png",_={class:"markdown-body"},m=e("p",null,[e("img",{src:d,alt:""})],-1),f=e("p",null,[t("長久以來都只有"),e("a",{href:"https://leetcode-cn.com/"},"中國版力扣"),t("支援完整的暗黑模式,而美國 LeetCode 主站則僅有在 Problem List、Profile 等少數頁面頁才支援,獨缺解題頁。")],-1),u=e("p",null,"某夜在深夜刷題的我突發奇想,說不定我可以取用力扣的色票,直接複寫 LeetCode 的樣式來實現深色模式,於是便誕生了這個專案。",-1),b=e("p",null,[t("原本也僅僅是寫好玩,就把它扔上了 GitHub,甚至連名字都是隨意取的。後來發現其實"),e("a",{href:"https://leetcode.com/discuss/general-discussion/544429/dark-mode"},"蠻多人"),t("都在跟官方敲碗深色模式。因此後來我決定把它上架到商店,讓它變得更容易安裝。對我來說,這也是個難得可以從頭到尾,完整地開發擴充套件的機會。過程除了前端相關的開發、打包、上版號,甚至也包含 UI 和 Logo 設計 、截圖和宣傳橫幅的設計、發布送審套件等。")],-1),k=e("p",null,"經過了半年左右時間,使用者人數終於突破了 1K,還獲得了藍勾 & 精選商品徽章,也算是達成了不值一提的小小成就。",-1),L=e("p",null,[e("img",{src:h,alt:""})],-1),C={id:"%E7%B0%A1%E4%BB%8B",tabindex:"-1"},v=t("簡介 "),B={class:"header-anchor",href:"#%E7%B0%A1%E4%BB%8B"},x=r('LeetCode Night 是 Google Chrome 的擴充功能,讓使用者在題目頁也可以套用深色模式,即使在夜晚刷題也不必擔心瞎眼。
TypeScript UI 組件基於 Tocas UI v4 建構,搭配 styled-component 自定義樣式 使用 Axios 和 GraphQL 請求題目列表 使用 react-i18next 本地化 透過 Webpack css-loader 向頁面注入複寫用的 SASS 樣式
此外還有附加以下便利功能:
反相圖片顏色,讓題目中多數白底的插圖,在深色模式中也不至於太突兀 輸入 Question Number 來迅速跳轉至題目頁,此插件會將題目列表緩存至本機,無需等待 API 回應 自動清空編輯器裡前一次送出過的程式碼,特別適合刷題狂人,避免不小心瞄到答案
',6),U={id:"demo",tabindex:"-1"},y=t("Demo "),N={class:"header-anchor",href:"#demo"},S=e("a",{href:"https://chrome.google.com/webstore/detail/leetcode-night/aaokgipfeeeciodnffigjfiafledhcii",target:"_blank"},[e("img",{src:"https://storage.googleapis.com/web-dev-uploads/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png"})],-1),w=e("br",null,null,-1),j=e("iframe",{src:"https://ghbtns.com/github-btn.html?user=ngseke&repo=leetcode-night&type=star&count=false",frameborder:"0",scrolling:"0",width:"150",height:"20"},null,-1),P={title:"LeetCode Night",briefDescription:"LeetCode 深色模式瀏覽器擴充功能",githubLink:"https://github.com/ngseke/leetcode-night",demoLink:"https://chrome.google.com/webstore/detail/leetcode-night/aaokgipfeeeciodnffigjfiafledhcii",period:"2022/02",cover:"/img/project-cover/leetcode-night.png",tags:["React","TypeScript","i18n","styled-components","UI/UX"]},V="",D=c({__name:"leetcode-night",setup(A,{expose:i}){return i({frontmatter:{title:"LeetCode Night",briefDescription:"LeetCode 深色模式瀏覽器擴充功能",githubLink:"https://github.com/ngseke/leetcode-night",demoLink:"https://chrome.google.com/webstore/detail/leetcode-night/aaokgipfeeeciodnffigjfiafledhcii",period:"2022/02",cover:"/img/project-cover/leetcode-night.png",tags:["React","TypeScript","i18n","styled-components","UI/UX"]},excerpt:void 0}),(I,T)=>{const o=n("icon-link");return l(),a("div",_,[m,f,u,b,k,L,e("h2",C,[v,e("a",B,[s(o)])]),x,e("h2",U,[y,e("a",N,[s(o)])]),S,w,j])}}});export{D as default,V as excerpt,P as frontmatter};
diff --git a/assets/legacy-42002bd1.png b/assets/legacy-42002bd1.png
new file mode 100644
index 00000000..4b38597f
Binary files /dev/null and b/assets/legacy-42002bd1.png differ
diff --git a/assets/login-3ac1d3a4.png b/assets/login-3ac1d3a4.png
new file mode 100644
index 00000000..541ef1f2
Binary files /dev/null and b/assets/login-3ac1d3a4.png differ
diff --git a/assets/login-7406d8e1.png b/assets/login-7406d8e1.png
new file mode 100644
index 00000000..b4577b99
Binary files /dev/null and b/assets/login-7406d8e1.png differ
diff --git a/assets/logo-animation-3865b365.gif b/assets/logo-animation-3865b365.gif
new file mode 100644
index 00000000..24065ebd
Binary files /dev/null and b/assets/logo-animation-3865b365.gif differ
diff --git a/assets/logo-candidates-4f73c7bd.png b/assets/logo-candidates-4f73c7bd.png
new file mode 100644
index 00000000..2e7b7dbd
Binary files /dev/null and b/assets/logo-candidates-4f73c7bd.png differ
diff --git a/assets/logo_en-8478642b.png b/assets/logo_en-8478642b.png
new file mode 100644
index 00000000..dca96e7d
Binary files /dev/null and b/assets/logo_en-8478642b.png differ
diff --git a/assets/logo_fb-3a5fbf8a.png b/assets/logo_fb-3a5fbf8a.png
new file mode 100644
index 00000000..b8d45afa
Binary files /dev/null and b/assets/logo_fb-3a5fbf8a.png differ
diff --git a/assets/manage-carousel-5d0aae2a.png b/assets/manage-carousel-5d0aae2a.png
new file mode 100644
index 00000000..561ddaba
Binary files /dev/null and b/assets/manage-carousel-5d0aae2a.png differ
diff --git a/assets/manage-member-9cbfc70b.png b/assets/manage-member-9cbfc70b.png
new file mode 100644
index 00000000..85293be8
Binary files /dev/null and b/assets/manage-member-9cbfc70b.png differ
diff --git a/assets/manage-research-b7b5b46f.png b/assets/manage-research-b7b5b46f.png
new file mode 100644
index 00000000..afffdeda
Binary files /dev/null and b/assets/manage-research-b7b5b46f.png differ
diff --git a/assets/manage-thesis-1289eceb.png b/assets/manage-thesis-1289eceb.png
new file mode 100644
index 00000000..c209f4ac
Binary files /dev/null and b/assets/manage-thesis-1289eceb.png differ
diff --git a/assets/mcip-28b770e5.js b/assets/mcip-28b770e5.js
new file mode 100644
index 00000000..3b52193d
--- /dev/null
+++ b/assets/mcip-28b770e5.js
@@ -0,0 +1 @@
+import{d as a,f as r,v as p,o as _,a as d,h as t,j as s,l as h,A as l,k as e}from"./app-033a95d9.js";const m="/assets/1-c36f7e53.png",g="/assets/2-4f25068f.png",u="/assets/3-ff7def49.png",f="/assets/legacy-42002bd1.png",E="/assets/facebook-messenger-bf65dd89.png",b="/assets/deployment-f152146e.png",B="/assets/logo-animation-3865b365.gif",x={class:"markdown-body"},k=l('樂台計畫官方網站 是以 Nuxt 建構的 SSR 形象網站。在此專案我負責的項目,包含定義網站架構與規格、UI/UX 設計、開發與部署。
關於《樂台計畫》平台詳細介紹請見MCIP CMS 。
',3),A={id:"%E7%B7%A3%E7%94%B1",tabindex:"-1"},N=e("緣由 "),S={class:"header-anchor",href:"#%E7%B7%A3%E7%94%B1"},v=t("p",null,"樂台計畫在初期並未規劃官方網站,但隨著規模逐漸成長,有越來越多人與音樂社團對我們感到好奇,這讓我們重新思考品牌形象的建立與加強推廣。為了能接觸到更多群眾,讓更多人可以透過 Facebook 粉絲專頁以外的管道來認識我們,遂開始著手官方網站的構思。",-1),C={id:"%E8%88%8A%E7%89%88%E7%B6%B2%E7%AB%99",tabindex:"-1"},j=e("舊版網站 "),L={class:"header-anchor",href:"#%E8%88%8A%E7%89%88%E7%B6%B2%E7%AB%99"},D=t("p",null,"在原本的前期版本,網站的定位只是一個陽春的入口網站,用來引導使用者加入樂台計畫 LINE App。因此當時是朝著單頁式網站的方向來設計,畫面上也只放置了 QRCode 等必要資訊。",-1),I=t("p",null,"為了盡快的製作出 prototype,當時選用 Parcel 這個易上手的打包工具。只要設定好進入點,它就會自動分析所有相依的資源,並自動封裝成 bundle,也省去很多繁複的手動配置。",-1),V=t("p",null,[t("img",{src:f,alt:"陽春的初版網站首頁"})],-1),P={id:"%E4%BB%A5-nuxt-%E9%87%8D%E6%96%B0%E6%9E%B6%E6%A7%8B",tabindex:"-1"},y=e("以 Nuxt 重新架構 "),M={class:"header-anchor",href:"#%E4%BB%A5-nuxt-%E9%87%8D%E6%96%B0%E6%9E%B6%E6%A7%8B"},U=t("p",null,"隨著網站的內容的不斷擴充,逐漸加入了「合作院校」、「聯絡我們」、「最新消息」和「FAQ」等區塊,我開始思考該如何優化網站 SEO,於是在後期決定採用 Nuxt 這個 SSR 解決方案,從頭開始打造新的網站。",-1),R=t("p",null,"在遷移到 Nuxt 後有許多好處,最顯而易見的就是預先抓取非同步資料,將內容渲染在頁面後才傳送到 client 端。其他像是 title、meta description 和 Open Graph 標籤等,也可以透過 Vue Meta 統一管理,一併預先渲染出來,有助搜尋引擎的爬蟲擷取頁面資料。",-1),O=t("p",null,"例如在社群媒體或是通訊軟體分享最新消息的文章時,縮圖和文章內容就能夠順利的被平台抓取,直接秀出預覽的資訊,提升使用者對連結的興趣。",-1),w=t("p",null,[t("img",{src:E,alt:"Messenger 分享連結預覽"})],-1),G={id:"%E9%83%A8%E7%BD%B2",tabindex:"-1"},Q=e("部署 "),X={class:"header-anchor",href:"#%E9%83%A8%E7%BD%B2"},q=t("p",null,[e("此專案部署在 "),t("a",{href:"https://vercel.com/"},"Vercel"),e(" 上,堪稱是最最無腦的託管平台。基本上只需要按照官方指南,在根目錄建立配置檔 "),t("code",{class:""},"vercel.json"),e(",接著在平台上綁定 GitHub 版本庫即可。")],-1),F=t("p",null,"在預設情況下 Vercel 就內建了 CI/CD,每當任何一個分支有新的 push,它都會立即執行建構和部署,並針對每個 commit 都產生出一個預覽用的 domain,方便驗證線上的結果。",-1),W=t("p",null,[t("img",{src:b,alt:"Vercel Deployment"})],-1),H={id:"landing-page-%E7%9A%84-logo-%E5%8B%95%E7%95%AB",tabindex:"-1"},T=e("Landing Page 的 Logo 動畫 "),$={class:"header-anchor",href:"#landing-page-%E7%9A%84-logo-%E5%8B%95%E7%95%AB"},z=t("p",null,[e("我為識別 Logo 設計了簡單 SVG 動畫,藉著操控 "),t("code",{class:""},"stroke-dashoffset"),e(" 和 "),t("code",{class:""},"stroke-dasharray"),e(" 屬性,僅靠純 CSS3 就能達到酷炫的虛線描邊特效。")],-1),J=t("p",null,[t("img",{src:B,alt:"Logo 動畫示意"})],-1),K={id:"demo",tabindex:"-1"},Y=e("Demo "),Z={class:"header-anchor",href:"#demo"},tt=t("p",null,[t("a",{href:"https://mcip.app/"},"https://mcip.app/")],-1),et=t("iframe",{src:"https://ghbtns.com/github-btn.html?user=ngseke&repo=mcip.app&type=star&count=false",frameborder:"0",scrolling:"0",width:"150",height:"20"},null,-1),st={id:"related-project",tabindex:"-1"},ot=e("Related Project "),ct={class:"header-anchor",href:"#related-project"},pt={title:"MCIP Official Website",briefDescription:"《樂台計畫》官方網站",githubLink:"https://github.com/ngseke/mcip.ml",demoLink:"https://mcip.app",period:"2019/02",cover:"/img/project-cover/mcip.png",tags:["Nuxt","Schema.org","UI/UX"]},_t="",dt=a({__name:"mcip",setup(nt,{expose:c}){c({frontmatter:{title:"MCIP Official Website",briefDescription:"《樂台計畫》官方網站",githubLink:"https://github.com/ngseke/mcip.ml",demoLink:"https://mcip.app",period:"2019/02",cover:"/img/project-cover/mcip.png",tags:["Nuxt","Schema.org","UI/UX"]},excerpt:void 0});const{projectMap:n}=r(),i=[n.value["mcip-cms"]];return(it,at)=>{const o=p("icon-link");return _(),d("div",x,[k,t("h2",A,[N,t("a",S,[s(o)])]),v,t("h2",C,[j,t("a",L,[s(o)])]),D,I,V,t("h2",P,[y,t("a",M,[s(o)])]),U,R,O,w,t("h2",G,[Q,t("a",X,[s(o)])]),q,F,W,t("h2",H,[T,t("a",$,[s(o)])]),z,J,t("h2",K,[Y,t("a",Z,[s(o)])]),tt,et,t("h2",st,[ot,t("a",ct,[s(o)])]),s(h,{list:i,class:"!mt-10"})])}}});export{dt as default,_t as excerpt,pt as frontmatter};
diff --git a/assets/mcip-cms-dcfd5ebb.js b/assets/mcip-cms-dcfd5ebb.js
new file mode 100644
index 00000000..73e3ffcf
--- /dev/null
+++ b/assets/mcip-cms-dcfd5ebb.js
@@ -0,0 +1 @@
+import{d as a,f as r,v as i,o as n,a as m,h as t,j as s,l as _,A as d,k as g}from"./app-033a95d9.js";const l="/assets/fb-cover-6fb81625.png",f="/assets/dashboard-cc7d8fff.png",h="/assets/forms-d0d210e4.png",u="/assets/edit-form-5c88d403.png",b="/assets/login-3ac1d3a4.png",v="/assets/pug-67d261e0.png",V="/assets/competition-1f600c5f.png",j="/assets/config-21adacbc.png",C={class:"markdown-body"},x=d('
樂台計畫 誕生於 2018 年之冬,是我與大學好友鎧企(K7)攜手開發的大專院校音樂平台。我們皆來自吉他社,體認到各個賽事在籌辦時的痛點,於是樂台計畫應運而生。樂台計畫專為音樂賽事量身打造,宗旨是建構更優質的賽事環境,簡化社團在處理報名業務的作業流程。
服務上線至今,樂台計畫傾聽來自各方使用者的回饋和建議,持續優化系統與擴展規模。
對於參賽者,樂台計畫以 Line App 官方帳號的形式,讓使用者輕鬆簡單地報名參賽、瀏覽各賽事詳情,甚至可以查看當前比賽進度。目前使用者數已超過 6,000 人,全台從北到南,共計有 26 所大專院校的音樂社團成為我們的合作夥伴。
而對於主辦比賽的社團幹部們,樂台計畫提供一套完整的後台管理系統。在此專案中我主要負責的是後台管理系統 前端開發,與一部分 LINE App 的 UI/UX 。
管理後台大部分組件取自 Vuetify ,得益於這些開箱即用的 Material Design 風格組件,讓我們可以更專注在功能需求上,在初期得以快速地迭代原型。
此系統落實前後端分離,使用 Axios 與後端串接資料。我與後端合力規劃並協調 API 的規格,當中包含了 Database Schema 設計。在樂台計畫草創期,我們根據業務邏輯並考慮 NoSQL 的特性,妥善設計出合理的系統架構。
本專案使用 Pug 模板語言撰寫,透過縮排便能很靈活地調整 HTML 的巢狀結構。
',12),I={id:"related-project",tabindex:"-1"},M=g("Related Project "),P={class:"header-anchor",href:"#related-project"},D={title:"MCIP CMS",briefDescription:"《樂台計畫》後台管理系統",period:"2018/11",cover:"/img/project-cover/mcip-cms.png",tags:["Vue","Vuetify","ECharts","UI/UX"]},S="",y=a({__name:"mcip-cms",setup(k,{expose:e}){e({frontmatter:{title:"MCIP CMS",briefDescription:"《樂台計畫》後台管理系統",period:"2018/11",cover:"/img/project-cover/mcip-cms.png",tags:["Vue","Vuetify","ECharts","UI/UX"]},excerpt:void 0});const{projectMap:o}=r(),c=[o.value.mcip];return(N,U)=>{const p=i("icon-link");return n(),m("div",C,[x,t("h2",I,[M,t("a",P,[s(p)])]),s(_,{list:c,class:"!mt-10"})])}}});export{y as default,S as excerpt,D as frontmatter};
diff --git a/assets/menu-d3272083.png b/assets/menu-d3272083.png
new file mode 100644
index 00000000..4f3f7d9d
Binary files /dev/null and b/assets/menu-d3272083.png differ
diff --git a/assets/mingpai1-50edfb95.png b/assets/mingpai1-50edfb95.png
new file mode 100644
index 00000000..e8c67e10
Binary files /dev/null and b/assets/mingpai1-50edfb95.png differ
diff --git a/assets/mingpai2-dead019a.png b/assets/mingpai2-dead019a.png
new file mode 100644
index 00000000..6904cda9
Binary files /dev/null and b/assets/mingpai2-dead019a.png differ
diff --git a/assets/mobile-bc89f109.png b/assets/mobile-bc89f109.png
new file mode 100644
index 00000000..a7b1201f
Binary files /dev/null and b/assets/mobile-bc89f109.png differ
diff --git a/assets/move-object-properties-99f2f466.gif b/assets/move-object-properties-99f2f466.gif
new file mode 100644
index 00000000..28a0ff29
Binary files /dev/null and b/assets/move-object-properties-99f2f466.gif differ
diff --git a/assets/open-editors-844d14f7.png b/assets/open-editors-844d14f7.png
new file mode 100644
index 00000000..2afc9416
Binary files /dev/null and b/assets/open-editors-844d14f7.png differ
diff --git a/assets/output-parserOptions-project-error-ceeaa787.png b/assets/output-parserOptions-project-error-ceeaa787.png
new file mode 100644
index 00000000..410e1291
Binary files /dev/null and b/assets/output-parserOptions-project-error-ceeaa787.png differ
diff --git a/assets/output-parserOptions-project-without-error-74ff031b.png b/assets/output-parserOptions-project-without-error-74ff031b.png
new file mode 100644
index 00000000..4202c4aa
Binary files /dev/null and b/assets/output-parserOptions-project-without-error-74ff031b.png differ
diff --git a/assets/project-e24afe95.js b/assets/project-e24afe95.js
new file mode 100644
index 00000000..ee74041a
--- /dev/null
+++ b/assets/project-e24afe95.js
@@ -0,0 +1 @@
+import{u as J,_ as K,a as Q,b as U}from"./useOgImage-203f60bd.js";import{d as P,o as n,a,i as W,F as y,c as b,w as I,k as T,t as _,b as r,n as X,p as Y,q as c,V as F,e as Z,s as k,v as ee,u as t,j as h,h as g,x as te,y as se,m as H}from"./app-033a95d9.js";import{u as ne}from"./useReadHistory-d98c4b5d.js";const oe={key:2},ae=P({__name:"TeamMembers",props:{list:null},setup(l){const e=o=>o==="黃省喬";return(o,p)=>{const x=X;return n(!0),a(y,null,W(l.list,(u,d)=>{var i;return n(),a(y,{key:d},[e(u)?(n(),b(x,{key:0,class:"underline",href:{name:"index",hash:"#about"}},{default:I(()=>[T(_(u),1)]),_:2},1024)):(n(),a(y,{key:1},[T(_(u),1)],64)),d!==(((i=l.list)==null?void 0:i.length)??0)-1?(n(),a("span",oe,"、")):r("",!0)],64)}),128)}}});function re(){const l=Y();return{frontmatter:c(()=>{var o;return(o=l.meta)==null?void 0:o.frontmatter})}}const ce={class:"space-y-3"},ie={class:"text-4xl font-semibold dark:text-ngsek md:text-5xl"},le={key:0,class:"text-xl font-medium tracking-wide text-black-700 dark:text-black-300"},ue={class:"space-y-1 text-sm text-black-700 dark:text-black-300"},me={key:0},_e=T(" Team Member: "),pe={key:1,class:"space-x-3"},de=P({__name:"project",setup(l){var L,V;const{frontmatter:e}=re(),o=c(()=>{var s;return`${(s=e.value)==null?void 0:s.title} | ${F}`}),p=c(()=>{var s;return(s=e.value)==null?void 0:s.briefDescription}),{ogImageHeadMetaList:x,shouldShowOgImage:u}=J();Z(c(()=>({title:o.value,meta:[{property:"og:site_name",content:F},{property:"og:title",content:o.value},{property:"description",content:p.value},{property:"og:description",content:p.value},...x.value]})));const d=c(()=>{var $;const s=($=e.value)==null?void 0:$.period;if(!s)return null;if(Array.isArray(s)){const[m,f]=s;return m===f?k(m):`${k(m)} - ${k(f)}`}else return`${k(s)} - Present`}),i=c(()=>{var s;return(s=e.value)==null?void 0:s.githubLink}),v=c(()=>{var s;return(s=e.value)==null?void 0:s.demoLink}),{pushReadHistory:A}=ne();return(L=e.value)!=null&&L.name&&A((V=e.value)==null?void 0:V.name),(s,$)=>{var w,D,M;const m=K,f=Q,O=ae,S=te,q=se,G=ee("RouterView"),z=U;return n(),a(y,null,[t(u)?(n(),b(m,{key:0,description:t(p),img:(w=t(e))==null?void 0:w.cover,tags:(D=t(e))==null?void 0:D.tags,title:(M=t(e))==null?void 0:M.title},null,8,["description","img","tags","title"])):r("",!0),h(z,null,{header:I(()=>{var N,R,j,B,C,E;return[g("div",ce,[g("h1",ie,_((N=t(e))==null?void 0:N.title),1),(R=t(e))!=null&&R.briefDescription?(n(),a("p",le,_((j=t(e))==null?void 0:j.briefDescription),1)):r("",!0),h(f,{list:(B=t(e))==null?void 0:B.tags},null,8,["list"]),g("ul",ue,[g("li",null,_(t(d)),1),(C=t(e))!=null&&C.members?(n(),a("li",me,[_e,h(O,{list:(E=t(e))==null?void 0:E.members},null,8,["list"])])):r("",!0)]),t(i)||t(v)?(n(),a("div",pe,[t(i)?(n(),b(S,{key:0,href:t(i)},null,8,["href"])):r("",!0),t(v)?(n(),b(q,{key:1,href:t(v)},null,8,["href"])):r("",!0)])):r("",!0)])]}),default:I(()=>[h(G)]),_:1})],64)}}});typeof H=="function"&&H(de);export{de as default};
diff --git a/assets/projects-34cb3ecc.js b/assets/projects-34cb3ecc.js
new file mode 100644
index 00000000..398fc9b2
--- /dev/null
+++ b/assets/projects-34cb3ecc.js
@@ -0,0 +1 @@
+import{d as p,o as e,c as A,w as l,u as a,a as t,b as f,T as P,_ as m,r as x,e as C,V as d,f as T,g as V,h,F as B,i as N,j as c,k as y,t as R,l as S,m as u}from"./app-033a95d9.js";import{u as E}from"./useReadHistory-d98c4b5d.js";const D=p({__name:"AlertReadAllProject",setup(s){const{isReadAll:o,clearReadHistory:n}=E();return(_,r)=>(e(),A(P,{leaveActiveClass:"transition-all",leaveToClass:"scale-0"},{default:l(()=>[a(o)?(e(),t("div",{key:0,class:"mt-2 origin-left font-mono text-xs",onClick:r[0]||(r[0]=(...i)=>a(n)&&a(n)(...i))}," Wow, you've read all of my projects 😳 ")):f("",!0)]),_:1}))}}),H={},F={class:"link-effect inline-block whitespace-nowrap text-lg font-semibold leading-none after:inset-x-[-8px] after:inset-y-[-6px] after:rounded-lg after:bg-black-400/10",type:"button"};function I(s,o){return e(),t("button",F,[x(s.$slots,"default")])}const L=m(H,[["render",I]]),M={},W={class:"text-4xl font-semibold md:text-5xl"};function q(s,o){return e(),t("h2",W,[x(s.$slots,"default")])}const z=m(M,[["render",q]]),G={class:"container px-4 pt-16"},J={class:"mx-auto max-w-5xl py-16"},K={key:0},O=h("hr",{class:"mb-6 border-dashed border-black-200 dark:border-black-800"},null,-1),Q=y(" Show All Projects "),U=p({__name:"projects",setup(s){C({title:`Projects | ${d}`,meta:[{property:"og:site_name",content:d}]});const{downplayedProjects:o}=T(),{isDownplayed:n}=V();function _(){n.value=!1}return(r,i)=>{const b=z,k=S,g=L,j=D;return e(),t("div",G,[h("div",J,[(e(!0),t(B,null,N(a(o),({title:w,list:$},v)=>(e(),t("section",{key:v,class:"mb-12 space-y-8"},[c(b,null,{default:l(()=>[y(R(w),1)]),_:2},1024),c(k,{list:$},null,8,["list"])]))),128)),a(n)?(e(),t("div",K,[O,c(g,{onClick:_},{default:l(()=>[Q]),_:1})])):f("",!0),c(j)])])}}});typeof u=="function"&&u(U);export{U as default};
diff --git a/assets/pug-67d261e0.png b/assets/pug-67d261e0.png
new file mode 100644
index 00000000..06189ed7
Binary files /dev/null and b/assets/pug-67d261e0.png differ
diff --git a/assets/raise-your-red-flag-6df7832f.js b/assets/raise-your-red-flag-6df7832f.js
new file mode 100644
index 00000000..2e59944d
--- /dev/null
+++ b/assets/raise-your-red-flag-6df7832f.js
@@ -0,0 +1 @@
+import{d as n,v as r,o as i,a as c,h as e,j as o,k as t}from"./app-033a95d9.js";const l="/assets/cover-c3bb2266.png",d={class:"markdown-body"},p=e("p",null,"數位影像處理期末專題。",-1),h=e("p",null,[e("img",{src:l,alt:""})],-1),_=e("p",null,[t("藉助 "),e("strong",null,"tracking.js"),t(" 的力量,在瀏覽器以 Webcam 遊玩,重溫耳熟能詳的團康遊戲,一起成為"),e("em",null,"那支舉起專家"),t("。")],-1),m={id:"%E8%A9%A6%E7%8E%A9%E5%BD%B1%E7%89%87",tabindex:"-1"},g=t("試玩影片 "),u={class:"header-anchor",href:"#%E8%A9%A6%E7%8E%A9%E5%BD%B1%E7%89%87"},b=e("div",{class:"embed-responsive"},[e("iframe",{class:"embed-responsive-item",src:"https://www.youtube.com/embed/du_2fcqPENo",allowfullscreen:""})],-1),f={id:"demo",tabindex:"-1"},k=t("Demo "),v={class:"header-anchor",href:"#demo"},E=e("p",null,[e("a",{href:"https://raise-flag.web.app"},"https://raise-flag.web.app/")],-1),w=e("iframe",{src:"https://ghbtns.com/github-btn.html?user=ngseke&repo=Raise-Your-Red-Flag&type=star&count=false",frameborder:"0",scrolling:"0",width:"150",height:"20"},null,-1),D={title:"Raise Your Red Flag",briefDescription:"以 Webcam 重現經典團康遊戲《紅旗舉起來》",githubLink:"https://github.com/ngseke/Raise-Your-Red-Flag",demoLink:"https://raise-flag.web.app/",period:["2018/06","2018/06"],cover:"/img/project-cover/flag.png"},F="",Y=n({__name:"raise-your-red-flag",setup(R,{expose:a}){return a({frontmatter:{title:"Raise Your Red Flag",briefDescription:"以 Webcam 重現經典團康遊戲《紅旗舉起來》",githubLink:"https://github.com/ngseke/Raise-Your-Red-Flag",demoLink:"https://raise-flag.web.app/",period:["2018/06","2018/06"],cover:"/img/project-cover/flag.png"},excerpt:void 0}),(x,B)=>{const s=r("icon-link");return i(),c("div",d,[p,h,_,e("h2",m,[g,e("a",u,[o(s)])]),b,e("h2",f,[k,e("a",v,[o(s)])]),E,w])}}});export{Y as default,F as excerpt,D as frontmatter};
diff --git a/assets/react-handler-type-3a8ff22d.js b/assets/react-handler-type-3a8ff22d.js
new file mode 100644
index 00000000..e5ac7210
--- /dev/null
+++ b/assets/react-handler-type-3a8ff22d.js
@@ -0,0 +1,197 @@
+import{d as e,v as c,o as t,a as r,h as s,j as a,k as l,A as p}from"./app-033a95d9.js";const y="/assets/hover-type-d5f8cd06.png",B={class:"markdown-body"},A={id:"tl%3Bdr",tabindex:"-1"},i=l("TL;DR "),D={class:"header-anchor",href:"#tl%3Bdr"},C=p(`如果某個 event handler 的事件參數,就只是單純拿來呼叫 preventDefault()
或 stopPropagation()
,那麼無論它是什麼元素,只要將 event
指定成 SyntheticEvent
型別即可,寫起來也非常簡潔。
import { SyntheticEvent } from 'react' // 記得 import
+
+// ...
+
+const handleSubmit = ( event : SyntheticEvent ) => {
+ event . preventDefault ()
+}
+
+return (
+ < form onSubmit = { handleSubmit }>
+ { /* ... */ }
+ </ form >
+)
+
import { SyntheticEvent } from 'react' // 記得 import
+
+// ...
+
+const handleSubmit = ( event : SyntheticEvent ) => {
+ event . preventDefault ()
+}
+
+return (
+ < form onSubmit = { handleSubmit }>
+ { /* ... */ }
+ </ form >
+)
+
`,2),d={id:"%E5%88%86%E6%9E%90",tabindex:"-1"},E=l("分析 "),F={class:"header-anchor",href:"#%E5%88%86%E6%9E%90"},h=p(`考慮以下處理表單送出的程式碼:
function App () {
+ return (
+ < form
+ onSubmit = {( event ) => {
+ event . preventDefault ()
+ /* ... */
+ }}
+ >
+ { /* ... */ }
+ </ form >
+ )
+}
+
function App () {
+ return (
+ < form
+ onSubmit = {( event ) => {
+ event . preventDefault ()
+ /* ... */
+ }}
+ >
+ { /* ... */ }
+ </ form >
+ )
+}
+
有時候會想把 inline event handler onSubmit
給抽出來,變成 handleSubmit
。
但直接取出函式當然是行不通的,因為 TypeScript 無從推斷出參數 event
的型別。
function App () {
+ const handleSubmit = ( event ) => {
+ // ^^^^^
+ // Parameter 'event' implicitly has an 'any' type.ts(7006)
+ event . preventDefault ()
+ }
+
+ return (
+ < form onSubmit = { handleSubmit }>
+ { /* ... */ }
+ </ form >
+ )
+}
+
function App () {
+ const handleSubmit = ( event ) => {
+ // ^^^^^
+ // Parameter 'event' implicitly has an 'any' type.ts(7006)
+ event . preventDefault ()
+ }
+
+ return (
+ < form onSubmit = { handleSubmit }>
+ { /* ... */ }
+ </ form >
+ )
+}
+
在某些情況,如果 handler 裡根本沒用到 event
這個參數,例如常用的 onClick
事件,那麼這時其實把參數直接移除就行。
const handleClick = () => {
+ console . log ( 'Clicked' )
+}
+
+return (
+ < button onClick = { handleClick }>
+ Button
+ </ button >
+)
+
const handleClick = () => {
+ console . log ( 'Clicked' )
+}
+
+return (
+ < button onClick = { handleClick }>
+ Button
+ </ button >
+)
+
然而在處理表單的 onSubmit
的場合時,經常會需要呼叫這個事件的 preventDefault()
,用來阻止事件傳遞。
因此我們常常得 「復刻」 出這個事件的型別,而不得不寫出冗長難讀的型別,例如 React.FormEventHandler<HTMLFormElement>
。
function App () {
+ const handleSubmit : React . FormEventHandler < HTMLFormElement > = ( event ) => {
+ event . preventDefault ()
+ }
+
+ return (
+ < form onSubmit = { handleSubmit }>
+ { /* ... */ }
+ </ form >
+ )
+}
+
function App () {
+ const handleSubmit : React . FormEventHandler < HTMLFormElement > = ( event ) => {
+ event . preventDefault ()
+ }
+
+ return (
+ < form onSubmit = { handleSubmit }>
+ { /* ... */ }
+ </ form >
+ )
+}
+
BTW 這一長串型別的定義,可以透過將游標放在 JSX 事件上得知。
',11),g={id:"synthetic-event",tabindex:"-1"},u=l("Synthetic Event "),f={class:"header-anchor",href:"#synthetic-event"},v=p(`翻開 React 的官方文件 可以得知,在 React 中的所有事件都是 Synthetic Event ,而非 原生的 Event。兩者的 API 雖然長得很相似,但實際上並不是同一個東西。
可以試著追溯 onSubmit
事件的型別來觀察這件事:
// node_modules/@types/react/index.d.ts:1383
+interface DOMAttributes < T > {
+ // ...
+ onSubmit ?: FormEventHandler < T > | undefined ;
+}
+
// node_modules/@types/react/index.d.ts:1383
+interface DOMAttributes < T > {
+ // ...
+ onSubmit ?: FormEventHandler < T > | undefined ;
+}
+
再往上查看 FormEventHandler
和 FormEvent
:
// node_modules/@types/react/index.d.ts:1303
+type FormEventHandler < T = Element > = EventHandler < FormEvent < T >>;
+
+// node_modules/@types/react/index.d.ts:1195
+interface FormEvent < T = Element > extends SyntheticEvent < T > {
+}
+
// node_modules/@types/react/index.d.ts:1303
+type FormEventHandler < T = Element > = EventHandler < FormEvent < T >>;
+
+// node_modules/@types/react/index.d.ts:1195
+interface FormEvent < T = Element > extends SyntheticEvent < T > {
+}
+
由此可知 onSubmit
確實是一層層地從 SyntheticEvent
extend 出來的。
可以再進一步查看 SyntheticEvent
型別的定義,即可看到 preventDefault()
方法:
interface SyntheticEvent < T = Element , E = Event >
+ extends BaseSyntheticEvent < E , EventTarget & T , EventTarget > {}
+
+interface BaseSyntheticEvent < E = object , C = any , T = any > {
+ nativeEvent : E ;
+ currentTarget : C ;
+ target : T ;
+ bubbles : boolean ;
+ cancelable : boolean ;
+ defaultPrevented : boolean ;
+ eventPhase : number ;
+ isTrusted : boolean ;
+ preventDefault () : void ;
+ isDefaultPrevented () : boolean ;
+ stopPropagation () : void ;
+ isPropagationStopped () : boolean ;
+ persist () : void ;
+ timeStamp : number ;
+ type : string ;
+}
+
interface SyntheticEvent < T = Element , E = Event >
+ extends BaseSyntheticEvent < E , EventTarget & T , EventTarget > {}
+
+interface BaseSyntheticEvent < E = object , C = any , T = any > {
+ nativeEvent : E ;
+ currentTarget : C ;
+ target : T ;
+ bubbles : boolean ;
+ cancelable : boolean ;
+ defaultPrevented : boolean ;
+ eventPhase : number ;
+ isTrusted : boolean ;
+ preventDefault () : void ;
+ isDefaultPrevented () : boolean ;
+ stopPropagation () : void ;
+ isPropagationStopped () : boolean ;
+ persist () : void ;
+ timeStamp : number ;
+ type : string ;
+}
+
`,8),m={id:"%E7%B5%90%E8%AB%96",tabindex:"-1"},k=l("結論 "),b={class:"header-anchor",href:"#%E7%B5%90%E8%AB%96"},S=p(`經過分析可以得知,其實以下任一種寫法都可以使 handleSubmit
相容 onSubmit
事件。
const handleSubmit = ( e : React . SyntheticEvent ) => {
+ e . preventDefault ()
+}
+
const handleSubmit = ( e : React . SyntheticEvent ) => {
+ e . preventDefault ()
+}
+
const handleSubmit = ( e : React . FormEvent ) => {
+ e . preventDefault ()
+}
+
const handleSubmit = ( e : React . FormEvent ) => {
+ e . preventDefault ()
+}
+
const handleSubmit : React . FormEventHandler = ( e ) => {
+ e . preventDefault ()
+}
+
const handleSubmit : React . FormEventHandler = ( e ) => {
+ e . preventDefault ()
+}
+
`,4),H={title:"處理在 React 抽出 event handler 時常碰到的 TypeScript 參數型別問題",date:"2022/06/06",tags:["React","TypeScript"]},P="",M=e({__name:"react-handler-type",setup(_,{expose:o}){return o({frontmatter:{title:"處理在 React 抽出 event handler 時常碰到的 TypeScript 參數型別問題",date:"2022/06/06",tags:["React","TypeScript"]},excerpt:void 0}),(T,x)=>{const n=c("icon-link");return t(),r("div",B,[s("h2",A,[i,s("a",D,[a(n)])]),C,s("h2",d,[E,s("a",F,[a(n)])]),h,s("h2",g,[u,s("a",f,[a(n)])]),v,s("h2",m,[k,s("a",b,[a(n)])]),S])}}});export{M as default,P as excerpt,H as frontmatter};
diff --git a/assets/register-0126b216.png b/assets/register-0126b216.png
new file mode 100644
index 00000000..87ab3e89
Binary files /dev/null and b/assets/register-0126b216.png differ
diff --git a/assets/rename-symbol-ac29a4c0.png b/assets/rename-symbol-ac29a4c0.png
new file mode 100644
index 00000000..ac3cc343
Binary files /dev/null and b/assets/rename-symbol-ac29a4c0.png differ
diff --git a/assets/reproduce-bootstrap-grid-in-tailwind-ce693ef5.js b/assets/reproduce-bootstrap-grid-in-tailwind-ce693ef5.js
new file mode 100644
index 00000000..1b206df6
--- /dev/null
+++ b/assets/reproduce-bootstrap-grid-in-tailwind-ce693ef5.js
@@ -0,0 +1,37 @@
+import{d as p,v as c,o as t,a as r,h as s,j as a,k as o,A as n}from"./app-033a95d9.js";const d={class:"markdown-body"},i=s("blockquote",null,[s("p",null,[o("此筆記提到的 Grid 指的是元件庫 "),s("a",{href:"https://getbootstrap.com/"},"Bootstrap"),o(" 所提供的的網格系統(Grid system),和 CSS 的 "),s("code",{class:""},"display: grid"),o(" 屬性無關。")])],-1),y=s("blockquote",null,[s("p",null,"轉換後的 class 屬性有稍微簡化,但我想足以覆蓋大部分的使用場景了。")],-1),B={id:"bootstrap-%E5%92%8C-tailwind-%E7%9A%84-class-name-%E5%B0%8D%E6%87%89",tabindex:"-1"},h=o("Bootstrap 和 Tailwind 的 Class Name 對應 "),A={class:"header-anchor",href:"#bootstrap-%E5%92%8C-tailwind-%E7%9A%84-class-name-%E5%B0%8D%E6%87%89"},_={id:"row",tabindex:"-1"},f=s("code",{class:""},"row",-1),u=o(),g={class:"header-anchor",href:"#row"},E=s("p",null,[o("→ "),s("code",{class:""},"flex flex-wrap")],-1),x={id:"col",tabindex:"-1"},b=s("code",{class:""},"col",-1),D=o(),v={class:"header-anchor",href:"#col"},w=n('→ flex-1
備註:
.flex-1 \n flex : 1 // 即 `1 1 0%` \n
.flex-1 \n flex : 1 // 即 `1 1 0%` \n
',3),F={id:"col-auto",tabindex:"-1"},C=s("code",{class:""},"col-auto",-1),k=o(),m={class:"header-anchor",href:"#col-auto"},q=n('→ flex-none
備註:
.flex-none \n flex : none // 即 `0 0 auto` \n
.flex-none \n flex : none // 即 `0 0 auto` \n
',3),T={id:"col-*",tabindex:"-1"},S=s("code",{class:""},"col-*",-1),G=o(),N={class:"header-anchor",href:"#col-*"},V=n('col-1
→ flex-none w-1/12
col-3
→ flex-none w-1/4
col-4
→ flex-none w-1/3
col-6
→ flex-none w-1/2
col-12
→ flex-none w-full
有需要的話,你甚至可以在 Tailwind 透過 Arbitrary Value 的特性來指定任意寬度,例如:w-[5rem]
、w-[50px]
。
',2),j={id:"w-100",tabindex:"-1"},R=s("code",{class:""},"w-100",-1),z=o(),M={class:"header-anchor",href:"#w-100"},W=s("p",null,[o("→ "),s("code",{class:""},"w-full")],-1),H=s("p",null,"用於強迫換行。",-1),I={id:"%E9%9F%BF%E6%87%89%E5%BC%8F%EF%BC%88rwd%EF%BC%89",tabindex:"-1"},J=o("響應式(RWD) "),K={class:"header-anchor",href:"#%E9%9F%BF%E6%87%89%E5%BC%8F%EF%BC%88rwd%EF%BC%89"},L=n('By default, Tailwind uses a mobile first breakpoint system, … – Breakpoints · Bootstrap v5.2
Mobile first , responsive design is the goal. … – Responsive Design - Tailwind CSS
Tailwind 和 Bootstrap 都同樣遵循著行動裝置優先 的設計哲學,寬度由小到大。也就是說,當什麼斷點都沒加的時候,樣式會套用到所有寬度的裝置(>= 0px
),而加上 sm
時會套用在 >= 640px
,再加上 md
時會套用在 >= 768px
,依此類推。
值得注意的是,它們兩者斷點的預設值稍微不太一樣,例如 sm
在 Tailwind 是 640px
,在 Bootstrap 則是 576px
。不過這完全不成問題,因為你可以在 tailwind.config.js
輕鬆的自定義 ,甚至自創斷點名稱。
轉換範例如下:
col-12
→ flex-none w-full
col-md-6
→ flex-none md:w-1/2
col-3 col-sm-auto col-md col-lg-12
→flex-1 w-1/4 sm:flex-none sm:w-auto md:flex-1 lg:flex-none lg:w-full
',6),O={id:"%E9%96%93%E8%B7%9D%EF%BC%88gutter%EF%BC%89",tabindex:"-1"},P=o("間距(Gutter) "),Q={class:"header-anchor",href:"#%E9%96%93%E8%B7%9D%EF%BC%88gutter%EF%BC%89"},U=n('在預設情況下,column 之間是沒有間距的。這相當於是套用了 Bootstrap 4 的 row no-gutters
,或是 Bootstrap 5 的 row g-0
。
在 Tailwind 的場合想要實現 gutter,必須另外做以下兩點:
為 row 元素加上負值 的水平 margin,例如 -mx-4
。 為每一個 column 元素,加上與 row 元素相對應的水平 padding,例如 px-4
。 row 的負值 margin 是用來抵銷在最左或最右邊緣的 column 的 padding。
',4),X={id:"%E7%AF%84%E4%BE%8B",tabindex:"-1"},Y=o("範例 "),Z={class:"header-anchor",href:"#%E7%AF%84%E4%BE%8B"},$=n(`< div class = "-mx-4 flex flex-wrap" >
+ < div class = "w-full flex-none px-4" >
+ col-12
+ </ div >
+ < div class = "w-1/2 flex-none px-4" >
+ col-6
+ </ div >
+ < div class = "w-1/4 flex-none px-4" >
+ col-3
+ </ div >
+ < div class = "w-full" > <!-- 換行用 --> </ div >
+ < div class = "flex-none px-4" >
+ col
+ </ div >
+ < div class = "flex-1 px-4" >
+ col-auto
+ </ div >
+</ div >
+
< div class = "-mx-4 flex flex-wrap" >
+ < div class = "w-full flex-none px-4" >
+ col-12
+ </ div >
+ < div class = "w-1/2 flex-none px-4" >
+ col-6
+ </ div >
+ < div class = "w-1/4 flex-none px-4" >
+ col-3
+ </ div >
+ < div class = "w-full" > <!-- 換行用 --> </ div >
+ < div class = "flex-none px-4" >
+ col
+ </ div >
+ < div class = "flex-1 px-4" >
+ col-auto
+ </ div >
+</ div >
+
`,2),ss={id:"%E5%BB%B6%E4%BC%B8%E9%96%B1%E8%AE%80",tabindex:"-1"},os=o("延伸閱讀 "),ls={class:"header-anchor",href:"#%E5%BB%B6%E4%BC%B8%E9%96%B1%E8%AE%80"},as=s("p",null,[s("a",{href:"https://stackoverflow.com/questions/34352140/what-are-the-differences-between-flex-basis-and-width"},[s("code",{class:""},"flex-basis"),o(" 和 "),s("code",{class:""},"width"),o(" 的差別是什麼?")])],-1),ts={title:"用 Tailwind 重現 Bootstrap 的 Grid System",date:"2022/08/22",tags:["Tailwind","Bootstrap"]},rs="",ds=p({__name:"reproduce-bootstrap-grid-in-tailwind",setup(ns,{expose:e}){return e({frontmatter:{title:"用 Tailwind 重現 Bootstrap 的 Grid System",date:"2022/08/22",tags:["Tailwind","Bootstrap"]},excerpt:void 0}),(es,ps)=>{const l=c("icon-link");return t(),r("div",d,[i,y,s("h2",B,[h,s("a",A,[a(l)])]),s("h3",_,[f,u,s("a",g,[a(l)])]),E,s("h3",x,[b,D,s("a",v,[a(l)])]),w,s("h3",F,[C,k,s("a",m,[a(l)])]),q,s("h3",T,[S,G,s("a",N,[a(l)])]),V,s("h3",j,[R,z,s("a",M,[a(l)])]),W,H,s("h2",I,[J,s("a",K,[a(l)])]),L,s("h2",O,[P,s("a",Q,[a(l)])]),U,s("h2",X,[Y,s("a",Z,[a(l)])]),$,s("h2",ss,[os,s("a",ls,[a(l)])]),as])}}});export{ds as default,rs as excerpt,ts as frontmatter};
diff --git a/assets/room-f8da92ed.png b/assets/room-f8da92ed.png
new file mode 100644
index 00000000..09cddfb0
Binary files /dev/null and b/assets/room-f8da92ed.png differ
diff --git a/assets/screenshot-1-81b8fb35.png b/assets/screenshot-1-81b8fb35.png
new file mode 100644
index 00000000..43cad73d
Binary files /dev/null and b/assets/screenshot-1-81b8fb35.png differ
diff --git a/assets/screenshot-2-9726f1a7.png b/assets/screenshot-2-9726f1a7.png
new file mode 100644
index 00000000..bd25b88e
Binary files /dev/null and b/assets/screenshot-2-9726f1a7.png differ
diff --git a/assets/shanlinliang-9edbff22.js b/assets/shanlinliang-9edbff22.js
new file mode 100644
index 00000000..6a89e720
--- /dev/null
+++ b/assets/shanlinliang-9edbff22.js
@@ -0,0 +1 @@
+import{d as e,o as n,a as o,h as t,k as a}from"./app-033a95d9.js";const r="/assets/cover-ca6a18cd.png",i="/assets/sll1-98c64e6a.png",l="/assets/sll4-b982d4df.png",c="/assets/sll5-a9b2ee83.png",p={class:"markdown-body"},_=t("p",null,[t("strong",null,"《扇林涼 Shanlinliang》"),a(" 是一個虛構的涼扇品牌,運用簡約設計文字營造出商品的高級感。")],-1),g=t("p",null,[t("img",{src:r,alt:""})],-1),d=t("p",null,[t("img",{src:i,alt:""})],-1),m=t("p",null,[t("img",{src:l,alt:""}),t("br"),t("img",{src:c,alt:""})],-1),h=[_,g,d,m],x={title:"Shanlinliang",briefDescription:"虛構涼扇品牌廣告《扇林涼》",period:["2017/10","2017/10"],cover:"/img/project-cover/shanlinliang.png",tags:["Illustrator"]},k="",B=e({__name:"shanlinliang",setup(u,{expose:s}){return s({frontmatter:{title:"Shanlinliang",briefDescription:"虛構涼扇品牌廣告《扇林涼》",period:["2017/10","2017/10"],cover:"/img/project-cover/shanlinliang.png",tags:["Illustrator"]},excerpt:void 0}),(f,v)=>(n(),o("div",p,h))}});export{B as default,k as excerpt,x as frontmatter};
diff --git a/assets/shouce1-193ed0a8.jpg b/assets/shouce1-193ed0a8.jpg
new file mode 100644
index 00000000..988a6dcb
Binary files /dev/null and b/assets/shouce1-193ed0a8.jpg differ
diff --git a/assets/shouce2-0a3f097e.jpg b/assets/shouce2-0a3f097e.jpg
new file mode 100644
index 00000000..b11d3af0
Binary files /dev/null and b/assets/shouce2-0a3f097e.jpg differ
diff --git a/assets/sll1-98c64e6a.png b/assets/sll1-98c64e6a.png
new file mode 100644
index 00000000..4077e5c6
Binary files /dev/null and b/assets/sll1-98c64e6a.png differ
diff --git a/assets/sll4-b982d4df.png b/assets/sll4-b982d4df.png
new file mode 100644
index 00000000..95689345
Binary files /dev/null and b/assets/sll4-b982d4df.png differ
diff --git a/assets/sll5-a9b2ee83.png b/assets/sll5-a9b2ee83.png
new file mode 100644
index 00000000..1da10979
Binary files /dev/null and b/assets/sll5-a9b2ee83.png differ
diff --git a/assets/store-f420ca97.png b/assets/store-f420ca97.png
new file mode 100644
index 00000000..8e407a4d
Binary files /dev/null and b/assets/store-f420ca97.png differ
diff --git a/assets/tabs-fc5ce7c7.png b/assets/tabs-fc5ce7c7.png
new file mode 100644
index 00000000..a3f8e08e
Binary files /dev/null and b/assets/tabs-fc5ce7c7.png differ
diff --git a/assets/terminal-lint-09df797c.png b/assets/terminal-lint-09df797c.png
new file mode 100644
index 00000000..4e9400db
Binary files /dev/null and b/assets/terminal-lint-09df797c.png differ
diff --git a/assets/tic-tac-toe-dd64aee5.js b/assets/tic-tac-toe-dd64aee5.js
new file mode 100644
index 00000000..08129a72
--- /dev/null
+++ b/assets/tic-tac-toe-dd64aee5.js
@@ -0,0 +1 @@
+import{d as s,v as c,o as i,a as n,h as t,j as a,k as r}from"./app-033a95d9.js";const p="/assets/1-ce441fa9.png",h="/assets/2-6e0e5062.png",_={class:"markdown-body"},g=t("p",null,[t("img",{src:p,alt:""}),t("br"),t("img",{src:h,alt:""})],-1),d=t("p",null,"簡直不可理喻的圈圈叉叉遊戲。在這個星球上同一時間,最多只能供兩個人類遊玩。",-1),l=t("p",null,"有附帶聊天室,提供玩家文明交流的空間。",-1),m={id:"demo",tabindex:"-1"},u=r("Demo "),b={class:"header-anchor",href:"#demo"},k=t("p",null,[t("a",{href:"https://ngseke.github.io/tic-tac-toe/"},"https://ngseke.github.io/tic-tac-toe/")],-1),f=t("iframe",{src:"https://ghbtns.com/github-btn.html?user=ngseke&repo=tic-tac-toe&type=star&count=false",frameborder:"0",scrolling:"0",width:"150",height:"20"},null,-1),L={title:"Tic Tac Toe",briefDescription:"圈圈叉叉亂鬥",githubLink:"https://github.com/ngseke/tic-tac-toe",demoLink:"https://ngseke.github.io/tic-tac-toe/",period:["2018/10","2018/10"],cover:"/img/project-cover/tic-tac-toe.png",tags:["Vue","gulp","Firebase","UI/UX"]},U="",j=s({__name:"tic-tac-toe",setup(v,{expose:e}){return e({frontmatter:{title:"Tic Tac Toe",briefDescription:"圈圈叉叉亂鬥",githubLink:"https://github.com/ngseke/tic-tac-toe",demoLink:"https://ngseke.github.io/tic-tac-toe/",period:["2018/10","2018/10"],cover:"/img/project-cover/tic-tac-toe.png",tags:["Vue","gulp","Firebase","UI/UX"]},excerpt:void 0}),(T,x)=>{const o=c("icon-link");return i(),n("div",_,[g,d,l,t("h2",m,[u,t("a",b,[a(o)])]),k,f])}}});export{j as default,U as excerpt,L as frontmatter};
diff --git a/assets/tixing-f836dc82.png b/assets/tixing-f836dc82.png
new file mode 100644
index 00000000..22a58212
Binary files /dev/null and b/assets/tixing-f836dc82.png differ
diff --git a/assets/tsconfig-include-a4dd9125.png b/assets/tsconfig-include-a4dd9125.png
new file mode 100644
index 00000000..9c064824
Binary files /dev/null and b/assets/tsconfig-include-a4dd9125.png differ
diff --git a/assets/tshirt1-4f8fb94c.png b/assets/tshirt1-4f8fb94c.png
new file mode 100644
index 00000000..3a45ccde
Binary files /dev/null and b/assets/tshirt1-4f8fb94c.png differ
diff --git a/assets/tshirt2-c61c550c.png b/assets/tshirt2-c61c550c.png
new file mode 100644
index 00000000..0ba366a3
Binary files /dev/null and b/assets/tshirt2-c61c550c.png differ
diff --git a/assets/typescript-as-const-fb61f8e3.js b/assets/typescript-as-const-fb61f8e3.js
new file mode 100644
index 00000000..73ca116e
--- /dev/null
+++ b/assets/typescript-as-const-fb61f8e3.js
@@ -0,0 +1,113 @@
+import{d as e,v as c,o as t,a as r,h as s,j as l,k as a,A as o}from"./app-033a95d9.js";const y="/assets/intellisense-e7791f21.png",A="/assets/rename-symbol-ac29a4c0.png",i={class:"markdown-body"},B=s("blockquote",null,[s("p",null,[a("這篇文章討論的是 TypeScript 特有的「常數斷言(Const Assertion)」語法,"),s("br"),a(" 和 JavaScript 宣告常數所使用的 "),s("code",{class:""},"const foo = 1"),a(" 是不同東西。")])],-1),D={id:"%E6%83%85%E5%A2%83%E4%B8%80%EF%BC%9A%E5%B8%B8%E6%95%B8%E5%AD%97%E4%B8%B2%E9%99%A3%E5%88%97",tabindex:"-1"},d=a("情境一:常數字串陣列 "),C={class:"header-anchor",href:"#%E6%83%85%E5%A2%83%E4%B8%80%EF%BC%9A%E5%B8%B8%E6%95%B8%E5%AD%97%E4%B8%B2%E9%99%A3%E5%88%97"},h={id:"%E6%83%85%E5%A2%83%E6%8F%8F%E8%BF%B0",tabindex:"-1"},g=a("情境描述 "),f={class:"header-anchor",href:"#%E6%83%85%E5%A2%83%E6%8F%8F%E8%BF%B0"},E=o(`🎡 TS Playground
想像我正在建立一個切換難易度的功能,有三種難度可選,先將他們宣告成陣列。
const options = [ 'easy' , 'normal' , 'hard' ]
+
const options = [ 'easy' , 'normal' , 'hard' ]
+
接著再建立一個變數 difficulty
,用來記錄目前選中的難易度。
type Difficulty = typeof options [ number ] // string
+
+let difficulty : Difficulty = options [ 0 ]
+
type Difficulty = typeof options [ number ] // string
+
+let difficulty : Difficulty = options [ 0 ]
+
但仔細一看型別 Difficulty
居然是 string
,這也太隨便了,因為這代表我可以把 difficulty
設成隨便一個字串,TypeScript 也不會阻止我。
difficulty = '安安' // 😑
+
difficulty = '安安' // 😑
+
`,7),u={id:"%E8%A7%A3%E6%B3%95",tabindex:"-1"},k=a("解法 "),F={class:"header-anchor",href:"#%E8%A7%A3%E6%B3%95"},_=o(`🎡 TS Playground
這時 as const
(const assertion)就派上用場了!寫法就像型別斷言 那樣。
const options = [ 'easy' , 'normal' , 'hard' ] as const
+
+// 或是使用尖括弧(tsx 以外的檔案才能用這種寫法)
+const options = < const >[ 'easy' , 'normal' , 'hard' ]
+
const options = [ 'easy' , 'normal' , 'hard' ] as const
+
+// 或是使用尖括弧(tsx 以外的檔案才能用這種寫法)
+const options = < const >[ 'easy' , 'normal' , 'hard' ]
+
如此即可得到預期中準確的型別。
type Difficulty = typeof difficulties [ number ] // "easy" | "normal" | "hard"
+
type Difficulty = typeof difficulties [ number ] // "easy" | "normal" | "hard"
+
搭配 VSCode 的 IntelliSense 可以得到代碼提示,也不再怕手殘拼錯字。(快速鍵 ⌘ + I
呼出)
map()
等回調函式的參數也會自動推斷出型別。
// 假設有另一個函式參數要求傳入 \`Difficulty\`
+const foo = ( difficulty : Difficulty ) => { /* ... */ }
+
+// difficulty: "easy" | "normal" | "hard"
+options . map ( difficulty => foo ( difficulty ))
+
// 假設有另一個函式參數要求傳入 \`Difficulty\`
+const foo = ( difficulty : Difficulty ) => { /* ... */ }
+
+// difficulty: "easy" | "normal" | "hard"
+options . map ( difficulty => foo ( difficulty ))
+
當然你也可以一開始就先把型別定義好,再以型別約束變數。 但我個人通常不太這麼做,因為這樣會重複出現很雷同的代碼,相當於需要費力維護多個真相來源。例如:
type Difficulty = 'easy' | 'normal' | 'hard'
+const options : Difficulty [] = [ 'easy' , 'normal' , 'hard' ]
+
type Difficulty = 'easy' | 'normal' | 'hard'
+const options : Difficulty [] = [ 'easy' , 'normal' , 'hard' ]
+
不過把型別抽出來單獨定義,還是有額外好處的。假設因為需求改變,需要把 'easy'
改名成 'simple'
,就可以活用 VSCode 的 重新命名符號(Rename Symbol) 功能來快速重構(從右鍵選單或快速鍵 F2
呼出)。
而這便是 as const
無法做到的,因此建議還是根據實際使用場景做權衡。
',14),b={id:"%E6%83%85%E5%A2%83%E4%BA%8C%EF%BC%9A%E6%97%A5%E6%9C%9F%E5%8D%80%E9%96%93%E7%9A%84-tuple",tabindex:"-1"},w=a("情境二:日期區間的 tuple "),m={class:"header-anchor",href:"#%E6%83%85%E5%A2%83%E4%BA%8C%EF%BC%9A%E6%97%A5%E6%9C%9F%E5%8D%80%E9%96%93%E7%9A%84-tuple"},v={id:"%E6%83%85%E5%A2%83%E6%8F%8F%E8%BF%B0-1",tabindex:"-1"},x=a("情境描述 "),q={class:"header-anchor",href:"#%E6%83%85%E5%A2%83%E6%8F%8F%E8%BF%B0-1"},S=o(`🎡 TS Playground
想像我正在建立一個從開始到結束的時間區間 變數 range
,一樣宣告成陣列。
const range = [ new Date (), new Date ()]
+
const range = [ new Date (), new Date ()]
+
還有一個參數是時間區間的函式。
function foo ( range : [ Date , Date ]) { /* ... */ }
+
function foo ( range : [ Date , Date ]) { /* ... */ }
+
接著試著把 range
放入函式 foo()
中,但卻出現型別不相容的錯誤。
foo ( range )
+// Argument of type 'Date[]' is not assignable to parameter of type '[Date, Date]'.
+// Target requires 2 element(s) but source may have fewer.(2345)
+
foo ( range )
+// Argument of type 'Date[]' is not assignable to parameter of type '[Date, Date]'.
+// Target requires 2 element(s) but source may have fewer.(2345)
+
分析跳出的錯誤得知:Date[]
不可指派給 [Date, Date]
,因為函式只接受長度剛好是 2
的陣列,但 range
卻可能是任何長度的陣列。
TypeScript 說的確實沒錯,即使原本 range
是用 const
宣告,但我在中途還是有機會偷改他。例如 push 新的東西進去,或是執行 range.length = 0
來清空陣列,這些都會改變陣列的長度。
`,9),T={id:"%E8%A7%A3%E6%B3%95-1",tabindex:"-1"},M=a("解法 "),Y={class:"header-anchor",href:"#%E8%A7%A3%E6%B3%95-1"},I=o(`🎡 TS Playground
試著加上 as const
。
const range = [ new Date (), new Date ()] as const
+
const range = [ new Date (), new Date ()] as const
+
檢查 range
的型別,會發現它從原本的 Date[]
變成 readonly [Date, Date]
了。
type MyRange = typeof range // readonly [23, 28]
+
type MyRange = typeof range // readonly [23, 28]
+
此後便無法再對 range
進行任何會改變它的操作了。
range . length = 0 // 🚫
+range . push ( new Date ()) // 🚫
+range . reverse () // 🚫
+
range . length = 0 // 🚫
+range . push ( new Date ()) // 🚫
+range . reverse () // 🚫
+
接著即可成功將 range
傳入函式。
const range = [ new Date (), new Date ()] as const
+
+function foo ( range : readonly [ Date , Date ]) { /* ... */ }
+
+foo ( range ) // ✅
+
const range = [ new Date (), new Date ()] as const
+
+function foo ( range : readonly [ Date , Date ]) { /* ... */ }
+
+foo ( range ) // ✅
+
留意函式的參數型別多加上了 readonly
關鍵字,那是用來告訴 TypeScript,這個函式不會去動到 range
,例如執行 range[0] = ...
來重新賦值陣列的某個項目。
而試圖存取超出陣列範圍的項目時,TypeScript 也會提示錯誤。(JavaScript 的情況則是會無聲地得到 undefined
)
// ❌ Tuple type '[Date, Date]' of length '2' has no element at index '100'.(2493)
+range [ 100 ]
+
// ❌ Tuple type '[Date, Date]' of length '2' has no element at index '100'.(2493)
+range [ 100 ]
+
甚至可以很明確地得知參數 length
的型別為 2
type Length = typeof range . length // 2
+
type Length = typeof range . length // 2
+
`,14),L={id:"%E7%B8%BD%E7%B5%90",tabindex:"-1"},N=a("總結 "),Q={class:"header-anchor",href:"#%E7%B8%BD%E7%B5%90"},V=s("p",null,[s("code",{class:""},"as const"),a(" 套用在不同型別的變數上會得到不同的效果:")],-1),K={id:"string%E3%80%81number%E3%80%81boolean",tabindex:"-1"},U=a("string、number、boolean "),G={class:"header-anchor",href:"#string%E3%80%81number%E3%80%81boolean"},J=o(`字面型別(literal type)加上 as const
後,型別就不會被「拓寬」,例如字串 'hello'
不會被推斷成 string
,而是會維持原樣。
let a = 'hello' // string
+let b = 'hello' as const // 'hello'
+
+let c = 123 // number
+let d = 123 as const // 123
+
+let e = true // boolean
+let f = true as const // true
+
let a = 'hello' // string
+let b = 'hello' as const // 'hello'
+
+let c = 123 // number
+let d = 123 as const // 123
+
+let e = true // boolean
+let f = true as const // true
+
`,2),R={id:"%E9%99%A3%E5%88%97",tabindex:"-1"},z=a("陣列 "),H={class:"header-anchor",href:"#%E9%99%A3%E5%88%97"},O=o(`陣列會被轉換成 readonly 的 tuple,也就是:
唯獨,陣列裡面的值始終相同,也不能被修改 長度永遠固定,不能執行 push()
或 pop()
等操作 let a = [ 123 , 'hello' ] // (string | number)[]
+let b = [ 123 , 'hello' ] as const // readonly [123, 'hello']
+
let a = [ 123 , 'hello' ] // (string | number)[]
+let b = [ 123 , 'hello' ] as const // readonly [123, 'hello']
+
`,3),P={id:"%E7%89%A9%E4%BB%B6",tabindex:"-1"},Z=a("物件 "),X={class:"header-anchor",href:"#%E7%89%A9%E4%BB%B6"},j=o(`物件裡的所有 屬性都會被加上 readonly,並且裡面的 string、number、boolean 和陣列值都會比照上述處理,型別不會被拓寬。
let a = { text : 'hello' , nested : { count : 123 } }
+// {
+// text: string
+// nested: { count: number }
+// }
+
+let b = { text : 'hello' , nested : { count : 123 } } as const
+// {
+// readonly text: "hello"
+// readonly nested: { readonly count: 123; }
+// }
+
let a = { text : 'hello' , nested : { count : 123 } }
+// {
+// text: string
+// nested: { count: number }
+// }
+
+let b = { text : 'hello' , nested : { count : 123 } } as const
+// {
+// readonly text: "hello"
+// readonly nested: { readonly count: 123; }
+// }
+
`,2),W={id:"%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99",tabindex:"-1"},$=a("參考資料 "),ss={class:"header-anchor",href:"#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"},as=s("p",null,[s("a",{href:"https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions"},"https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions")],-1),es={title:"讓 TypeScript 的 as const 救你一命",date:"2021/12/03",tags:["TypeScript"],original:"https://hackmd.io/@xq/as-const"},cs="",ts=e({__name:"typescript-as-const",setup(ns,{expose:p}){return p({frontmatter:{title:"讓 TypeScript 的 as const 救你一命",date:"2021/12/03",tags:["TypeScript"],original:"https://hackmd.io/@xq/as-const"},excerpt:void 0}),(ls,os)=>{const n=c("icon-link");return t(),r("div",i,[B,s("h2",D,[d,s("a",C,[l(n)])]),s("h3",h,[g,s("a",f,[l(n)])]),E,s("h3",u,[k,s("a",F,[l(n)])]),_,s("h2",b,[w,s("a",m,[l(n)])]),s("h3",v,[x,s("a",q,[l(n)])]),S,s("h3",T,[M,s("a",Y,[l(n)])]),I,s("h3",L,[N,s("a",Q,[l(n)])]),V,s("h4",K,[U,s("a",G,[l(n)])]),J,s("h4",R,[z,s("a",H,[l(n)])]),O,s("h4",P,[Z,s("a",X,[l(n)])]),j,s("h2",W,[$,s("a",ss,[l(n)])]),as])}}});export{ts as default,cs as excerpt,es as frontmatter};
diff --git a/assets/typing-typing-876fd270.js b/assets/typing-typing-876fd270.js
new file mode 100644
index 00000000..25f3ecba
--- /dev/null
+++ b/assets/typing-typing-876fd270.js
@@ -0,0 +1 @@
+import{d as g,v as e,o as E,a as B,h as A,j as o,k as s,A as a}from"./app-033a95d9.js";const n="/assets/menu-d3272083.png",h="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPAAAAAeCAMAAAAlxWNSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAACZUExURf////+Ttv75/DG2WbSK1SKxTACi6DE6t88QGv9/J7iR1xCo6v7y89IdJ/Xz+/+HM/+Zuj1Gu/P89fH7/v/48+no9/vj5NnF6v/w5fvH1OX36f+/kz+57cWm3/7cypid2/+hv+eGi/uxxZDYpdtKUmNqyf+fXUtTwYDR9Mbs0f+PQuH1/dUtNlfDdrzn+tXx797b8njO8+mOkywZf48AAASrSURBVFjD5ZnZdtowEIZlGxyDZRsvOIQEQmjabGRp3//hqtFotxzknly0p/+FYYxZvmis+WdC0jR/IT49pumNGf9Y2NoP3/I6n3+Tz5tknmyHl9xn2R0hV1l2T0hfZEVvvFYuZ8uSkN1sdknI5Wy2c07pCx8uLNVr+0voKlpR+1R8iCL8rv8OOM/z97Z1f1fbto95roG7rhsAd531lqZpALgR2jLg96axfwilAEzpkQFTumfAe0pjE3gdx0AXxwAcx2t9SuOWaxf4oyxjzRZz4NgSBWD2CGgvN3n+5gCzU4+t/jt0z1Xl8C6qqjIXuXlNkjlTIoXPn0zeU1FkWVYU8gGPVwbwbLlkBziKB3W81Mtb1xeO6rr+0Mu7WkVRtHKEp/h3tXmaDoDT9NEIu+eFTxYwxx3IBs58soBHdTmWz0LqdRp9IgSGFW5twQob+bwZAe66WObzdgS4kWlNae8HPsqsDgEe5rMElll9HpggoKU347beePJZZfU1v2SLKexTkuA+1mM+e8Sy+lco8NqTzyqrb0OBMYUtmSm+GcHlksDzcQngEVyuu2DgEVyu2ykr7AKbK1zJ5bTW1gVOnO1KPZ8KjJuUkAo0cM1lrS3GGhg3KUt4St3DqSvjHhbA7Ia9Vry9LFMKGGoQ6onxbmXwOh34g9ckwVvG4qwChhrE9F3yPqxlrIAPlJchi5eVqaMEHuKCBsCEaOCNNCIaWLkMAG4GXiQceM1dhwCOSewCo8vQwKWMNXDMjYYNTAgCszrsB/4p6zAAY9G9xkyuqmcA5s8nARdKmRFYwCyFAVhW4B376TujDk8ANrN6tToo4Dz380JW30hgSGHwMF23XyyeNzxA8zUFGGwVCp2W0L0BDLaKmyXhtESwmwwM7zpKXpbi8EEInH4iDbyRAADcGfZ6ErAyzuilLXutvTQKvbTQHwAjoABWMQJj6TWXVsSTgM9vWgBMucsQwCz4A+CATUsAYlazfLaBoXlg+ql4VTwFOKAsAfA9dxmiWzoVp34ycEBZEoAsq6+i6Ig3hgn8IttBAfxit4eBwOeNR9F728OpwOeNx0G1TgCsPmUK8PlNKwR4720P3U0LJTYt1FRg1RuKFUZNAA4oSyHWsvC2h25ZEpoZwVRraTaFVhAMfN54/DNeOmjTCrCWfw+wx0grOx1alkKah78FGEc8PuGIJ8h4/EvtIQ7xvMBiavklwGMDgEQNAEKAlyPI7AUL2K3DLvDB2x5+IXDAiCcAmCXjyCJ/SPOAwGC2TKclpj635uDS0x6SLwQ+P8QLASayF3Sl5s4CeG176dIFJt72UAGLHQsfzKNVh3GrqhZG4AInalKrjhr4NFaE1UwLfufOO6a1gOv6AYAxkVlQysmtDayKsGoPJTCUoRve/75BEeb98ZvjpbEMXUunBVPMveG0BDDL32/z+WvTvPNmArL8yZjCw9TySOkdt1nMbJ16ecqcovsG8cQAhnyGC8vy9uLie8kDs3nQHwTOKo571R4S418tOIcG4FbOqV0vjXTopWFOvTe8tAQmBIBlu9gkw7k0K4R33EgzO32i8pQlz79aLGC13ACsXnGBlXfuTW/9HwL/Bpla3N+gKOCsAAAAAElFTkSuQmCC",r="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAAoCAMAAAACNM4XAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAABsUExURf///8bd4Nnhx/jrx+KrG/f49OPk2WKEHI2MYx93gXWTOOzx6zqHkPH3+JuadeW1Nv768eOwKJSTbPz14YihUrLDj5C8wSt+h/79+2uLKVaZoMjXwMbFtqmni+nAU/DVi+rq5UuSmnR0dCAgIGjvxwIAAAFTSURBVFjD7ZbbcoMgEEC9RNRs1CBp0aiNSf7/HyssKgk6bWcamXY4TywMHtEF1vMcDofjD1IRk0objwIDos8nvkGmPz40wcdHefJMCdrMIDY46eJzauBrw4zunimONsUA/YKYAKjPAdAsiAHUqw2NqyluAPBrV4yFppgyxryyLBOToRf/41td16Y4rut9JIazK+fpApy/y+khpaZ3MFPqJWsocbyCEh/SFZR4t4YQ5xK1cAwexHuJ8o3BLD4g6MM218WFBBdOMUBx2UeCVno72YZcE5+wC82NDBpd7GcCH70gg7MmLthR8CHFF9kW+eZNKazEU6LPYuxCcTAmuiaWXUqcjYk+i3HvKPGU6E68qdhWcv3OduI/306WDpCXHJn8G0emjUuiEJfEl9di9d/u481Kn6DL88fSZ6PKru9ut8hGTUmS+x1siDPStpWr6R2OF/AJdINBQ5ZvaVwAAAAASUVORK5CYII=",c="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAfIAAABQBAMAAAAQHOqyAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAeUExURQ82iP///y1PlktopfDz+MPN4uHm8YicxB1Cj2yDtmgLuo4AAATFSURBVHja7ZrPTxNREMc3JaZ43FjacGsWBLyhRdRjYdF4K2iRa4EqR0ENeiM1QL1J1OCf60zf95V5w9tW9GBSZy779s379cnu5s3O+yZpmlaTJJlKQzuguvuqrkZ1t0b4va0lzkb551F+S+VN4buDvvNhl0rdVU/F+pNNd9ztDJXLcn1i/OqJ6w+MxMiN3MiN3MgnkzzLsrnXoW1R3UWMHP6dArIq+Xax+lUqNyP+PRp7BfdHVN6mOu+fpXumPFPr4Tpy3ZPkPNZXkGOMIfkuNeb5/fhkXWC+oetdR57meb7RD22B6poR8gT+kwLyO/ANlkPlw4hfjl2h8uN+v9cR97voK42qSvvkk+Q7Yi4q9pqCnMdh/3s3Pltv32Geu+mT9Oo1kTYfoaqpNlFy1eYw4j9TdctXb2vwtSgrNdXX0rruH5Kn8B+68SN+IzdyIzfyCSdvt9uv1DSnVLd5Q/Iq9Xmh2vygOrk7zjYajYdi7C6Vv1FdU5F/boR2IsifY4x3dEWEk3AbxqDLIz/OHt18wPjX/T6Gi9n9G5LXCsbZvEEMp/0ihis1IzGmiOF8jBZYJIZLjdzIjdzIjXxiyY+Pjz/xNk/Xd1jpFyr/pOsiXf2qqsfOjtDmHPfSz/1v0/Uj2nB/jiIu6epXXYPf99d+Sf4GbToF5H58JqcxPhWRX7phlN8/lynxzM4wsozBfPAjcyYt5U+Qs5E5GZUzuRbjJcofi9E2C8gjFiWP+43cyI3cyI18csmn6cf1gXdTeVmQV+j+CX5uH6Oe/59/4B+5I8hLqk1dkNX457jtbEf8v7PxFDrzsAFfs4D8YTs0vZ/vif93gfkoJC+yQ3EGE4vhfIw0M3oYR67OaMQ7FZDdJIaTiRFN3oqvpWzkRm7kRm7kE0vOGyGqbmNf3MI975cd5OPbBeRd5NMr6BvLt6P7oH8JuW8fL8xF8unj8u2DyAmxhDQ/D9s69nKe3//D41iBl7Aa5mTKYzQPf3vGUhv9XgTko/wFzzQa40XOWJJQLWLkRm7kRj6B5HmeP4X4SJKzNmkFWqQU+qlnQrPEmqTADz3Sulrue9GmpnRTmFZqm4L5he6pL8l4nhzaJ9l/H/WSnOf35KyNEuQQiXUV+QI0ccPMA5WXlB6uJPwDMmjZpPWozufkK6yF89kPjKP0bMH8/pluOz3ckHw9c7Ya9u+ielGS8/x+LUDw5EyQhmKDtTE5l0gM9ttnLFrj+PYPcjIFOZ/qifhoD0Z/WUZu5EZu5P8Hud8OsGusRMinnTw69SrquiSHfLqO3S5DuSdU3BUovOuKPFNK73XsWFsYh30PaPOV5CWo0I/QpuPE6plUedevdrMsu9rdgOHJU6W8bkbIy6pN4Id8miOZM0QULUQSXsXNkcwpVNyS/CUFU0sikhkovRFJ5ei/LyKiwX6PtVRE9JK7SKUs15e48dme+ncCKm9PHrFW5IxlnF9qHIUGMcjJRDQTI3MuywVfQ8SuaSCTUDOhslFGbuRGbuQTSs7HtwXkfArryapSPj3CzyruFeX3Ku6aS/cOVNrf0Qan0JVv4hTZZ40vUNYqcNlfC821yruBU24chjdClff/aUZu5EZu5EZu5EZu5EZu5EZu5EZu5Eb+7+0XCYyy6Wely/cAAAAASUVORK5CYII=",D="data:image/png;base64,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",p="data:image/png;base64,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",m="data:image/png;base64,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",b="data:image/png;base64,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",l="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAWgAAAC9BAMAAABsT/rOAAAAJFBMVEUUFBQMMDhkZGTIyMgAAAD/yA6goKC8AADurgB1AACmewD///+3s1MDAAAKSElEQVR42u2dwY7jNhKGSdFaIDdOvHP3aF+Ajl6gDx1Mek5qIL7n5HMODfQ0cg0wnT0HaCTnBTLIC+zrLauKpIqUNK22LLe4I2fGjl0W/alUKlb9pjxChJsWsn0i4fmiTf4NzC60XLhJ1dncTNbQyuQTHsrE0FmciB46OJq/QQu27ZJMSKtbR7M3SMG3XZIJaaWDloK/QQu27aJMW6R1sa2ZwT9o/rAQk3Oxczi3aL7RskwumLeGHRDJN6VXl2VS+8Au+fnahr4UizMpoz205nlF8sy4NJMy0jmcTzuap3O5OJN1sSaH84lTRuXf4kwWWrYOj8IpmoUWZXLZI09P5xnTOWaPHPN0jjNilrVHjlVelvV0lp1Llj1igM5EFkug85DFEmjxJipRooJlOab/B+gswyPHE3Eg1xztLTtZDKBzk8WO8HjMTBYDXn3MTBY7wv0xM1ns6O76C/OtHYReVcreGUUmhXYj0BtGKL8Vtndmflns6Nw90AIZVdeGOG0bRM+UfVnsga6ua/tg90cL+sbEfbsztyx29IE91Gyicx0KedqBKQsLz/dK4VZk1MLMLosd3W2gra+No7WBAt6kPQCTdbgyxsBObdH97qup+Gu1eWSxGLoroBhyIMREHQKCdsTugULovcIDYt+IgWRml8Vi6I5UpTxgrQAm8MDJolRtYxzYtz5mTBLTM8liz3gaMCheFTzWdFoajFsX0jZ1kIuVyyDc1fPIYs/EtM8eRm0V7IJCbxuJTlXufMQHlz1E5OeZZDEsPDx0zxkdPC0M2wUNA3tPS+Gyh3C5sXX1TLIYlnjHL+Rp8qYNDKO0cJ6uhaHpBrKJfRnytP0U1abDeWUxcLI+/oRHt29GDN40SkkHrTS427pUKToSyp3vBnOHCefrXLIYQEORB9A99cCesofREK0Q0xAee3y7xCSn3EyvMcqNxDSoZpbFLPRP/zgKOKCyzkUWY9C6zkUWo4QHvFLUuchiBA2RoT10BrIYTS1QAwXosc1z2byWLOahdQs9VqYob19XFquHTIa2MsqVy8xUHg63VKkKLEb32m8Bn/kmZGszlyw2CI0FNc6BxvRDwztca2CoXzABGl7DmnsWWaweMEHJYbbw4BynLWoDwE0ID1+eaGprfKuDFYglBujtLLJYPWCCeZKKe6oopODQeCLy5tD4sskGk6SNwNNSnVsWCy1gKHSYSUEdXWtb7pM/0VQ2FtelDk2NLTrUyNB1KUE1NnlaqzlWi0XQ3IRBCcUeeZpMAA2u1m6+xWiWRGuoYNraw1N7aInFyTllMXyMoLlJKSSqnac1nn4HgnYDqho9jeXdnsLDutx+pqnJ3QZq1xlWi3HoyGQbFjiRJHlaCg8N3G5Ag56WREv6h/EdMfY8e+Ej62yyGN5z6MikoC6FQ4ye1n4aBOhbn4bdMUBa6btFRUqTq3iVmGG1GOvuUllMUWoQvkmw0BDV+Ohcbd+yp0nFUKu1pU4SxzKzrRZj0LHJuCwALS21QRAet/b+lk2acAwoV8MWkOFIK8Hcok3QA8+7WqyFTkwgbGBTGPoRjGmNedprINTcCJrN6/DMhBx+blmMencPndVqMTiHIHizWi3mobNaLeZTXlarxTx0XqvFnKPzWi3mHb2uFltXi4l1tdgryGJicabRPeK6WuwCstjiTOMVpnW12Fmh19ViFzoR14soZ55c1osoL1UwrRdRXqwJGNNuLci0XkT5NV5EWVXBVJ1HFruACaCdqTqPLDa/qWo9jf/7KhdRXtPtauxWQOpMDvryF1FuGPSorYCUTFVVuVC58EWUm2/++OPp3t5+HruVBXUmBz1RFquq9lQZOYFtbhz03dW4aQ9AtXusKFSmyWIx9JhSYfPNn3//dX//8fHT/d24AqNygRygp8piVbglJvfqrrPV5sZCPz0g9LhSjkavYkdPkcUS6NbUQidbbT58fvz0YD39+OlOjCqaU+jJslgKLfkxReh0K4S+f4igv9ieVPHtDLJYCu1MRft6utXmw3+uf3h8/PX6hxY6DFhgPBU7/lkp9HRZrANNpqLzejD1QvsBEVp8u2OfNejo02WxLhyaeqHJtPnw9+dHvDFoN2DYbDcMPV0Wi4ZrTdHH7OKt+qGlHoCOPiFxwomy2CnQN38SNOTpmaH7w6MHWstBaAqPXujB8OhAT5fFcKx/1d9FOU934jA6Ee3kArfPHNoNWHT31D2vaw89WRarOHTVmvqhXcrrg3YDFmG0Xui2WpoiixHzd/CnYiWT7nwQn1yuMT7+/df9HVXUbMAWOlC7ofBPqEsnyWJdaH6QE2g/jfdA+wGLdrRdNLcE6DPIYv74B2h+OnHvRFu1TQAfsNjpgrngXTqLt9ATZbEOtB6GbrdinQsbsNjJgo2204ln6uSYnSyLRcwh3NocwKDjJgBvv/MmwG60+5YP9y6uF+v07DhVFosdHabYDnTSbjHodkCALvhoOx3Vi3WSh0otGlGeIIslzD6FFglzFW1lC6abp1/++/B09/2PV+2A7Z56aMnzap1mfFhJae9eLoul0GmJXbdThe6D/pUNWKSHbedMVQ+0NbwV3tMvlMWq3kmky1y9Y1sBNEXH9z+yAXugpex8yHMSz/OyWFX1UvdA7/QgtB+wSMfadZsjVi2dLIulvkmhq35Pv7+7AQXh4ff3d1dswKIDLVkbWr/M08MxPR46iukYWqeuTmOaBuzE9Kmy2AvCg+/4P6+v29mFDVgk0LLb8VdjRfXhPO2LRZc6fProheZ5mkPrwahO8nRaL54qi8XQXnHrDw8+gW3ef/zt6eOnu6vO3Fakx0b2Qk+RxSINRXttcwCa1x4tdFJFFJ2zQPdFxxRZLIL2KnJf9kJoVuXdPN1d279X477hrFINZZIsFlDZnR6EbsvfFnr8d8m8Tpgki/VBy2Ho0Ghs4BS0OeTqBd/as4psoiyWQo/qER003L1kfUQrL02UxRyv5tDPduO8ef5CX939LBd+Z5LFpEhlsX7oaWt+fD83XRYj3DdRWZCqPl8QhF5ich9xjtVil4MeZZoSHifHwFTTOH16YaZx3wSIdbXYZNO4b7e+jtVib/Hi4EY3B/ukPMOAL5bFTjDdHsqyEYfy0DSouEwe8AKrxUrxVjQNyEOgs5SZrBY70GW2cA+7cI5lDWL21WJlqRsb0RAjAJ3FajH4DYTmtnQx3TR5rBYDWps9RIPQZRY/uV+WJca0/Q8zXpPFT+4fDkLblNeQv23+yOAn98umlOUt/LIAxIZIXL3+5P4Z8/R6EeUJstj0UmG9iHKcLLbUenq9iFKvF1GK9SLKBSlM60WUqyy2ymKrLAYYUCzD49aI9nFPP0nlBI+ZZLETTfSzSfDzWsL/lf4128EJ94NNM8liJ5oYrP/rdkR2oZfSBPiQUC4kfKhAUwjhsTVzymLrRZQXksXE13MR5SvLYjObZpLFLuPp9V+ivFD2kM7jWeRp52gqPnKRxbyj/T85kMXNhD3ICVouVvt6zrTI9mSEaXmN4DOm/wENn8g+SfUj0wAAAABJRU5ErkJggg==",d="data:image/png;base64,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",W="data:image/png;base64,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",L="data:image/png;base64,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",F="/assets/char2-9248d6c0.png",Q="/assets/char3-5ba16f0d.png",x="/assets/game1-d689a75e.png",R="/assets/game2-ec9b0a6e.png",C="/assets/game3-ae563b09.png",Y="/assets/game4-ad0db896.png",k="data:image/png;base64,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",y="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQIAAACPBAMAAADjOMjRAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAPUExURRQUFP/ID8jIyCAgIAwwOJtnW3wAAAOzSURBVHja7ZxtkqQgDIaDzQHIDSxOYFXPAfix9z/TJgQEZ6Z6xQ8yPYvVIgJVPPMmINFRAAALsKYAoUr7VK3HUFfajlWfjhusnlU5k5tIpk/V2iTm1iac61RVmlC2NOFsn6qqCeWrJhA6VdVN4E99YjtVSalX2WoraBJEETyobJUIugQkghJAJYIewZOShURQJHguQD+rSfAEptAmeOoTLOoEz0EwCAbBIBgE+wgMogNAxHzu+Bf3XGvQpZpYkmtTk5cED3UCOEDABVQkxbkrxkLZEoypzl4RPN6SYDVEgkk0jBFRvprq5Ur1HQmM9Jh7LfbIMMk1zQ4jvCtBkltUzu5oihX2DsR3JpDORGLJuM1ozEw7R+NbEmyskDsvs7LJM1I1Uf9CApQx6JLYrrpKlVn51tF4JUE45IlF2mgF3AxB89kgrwmsOgGEY6MxjzSzjkn3xQrZP/9BYNUJQHulukeDmwmCNoFVt0I4QEB+Bw4cuRkC5Wgg7vC4Sz2RendoHPXqZAMHnQnob6aNBicC54zD7gTx73aiRsp2JTDSLTqxh2Ml+hJcPB88BgGo38F4jLs4g2AQDIJBMAgGwSC4mMAZY3jNTEkMYzhgBryJYL1ZV9+kc0bWzVxAO0aWltV7CwFuN6HiDfgnOHKzAe8hMLFTTmR3iWDVgMNHShujyLMEIHfZUwxDARy7wV1WYAIs+6oBm0Bu67MURMOhZEcCjFYQgUQDIsIbrRA9sCKIKshO5RxEkwbgOhKIJ1KK3HflmzdagdMNQcxhvLEiD3nYMfsRdJ2V02jMlvgpBNM8efDTNME0+bsJvp2VY9+czn66/9r43ZWJNJijBgyhcnVmDThhHUCLwLMv9PGDX7xKWwbBDyCAQfC02gRL0Cb4OKVBfrTMyyNaocQnP2ja4oXQ7gfVE20JVPIeCWiJBE0E0E5Qx0w5ZohIMW5sXD8dIWCRTV4ncviSNDASPZoYvO0nWM4SOFOsACl0wPsJ8pPuTBCtImvlThpUBMUKBzWAs1bYjAUpbVpFX0kgsZtpHQtw0grrP8Nhipk6abASXDArn7TCdQTLQQIKEmK8ALRSpniBzvw8Ty0r90wAB2flaY0VZk5o5S4BTDNB27s8ZhMzcawwJ4J5nkkL307wcfi1tk8akBXmIwTL8ffaJu7Yp86LH/hGAt332tIxqBNYdQL4nwmCOoHVt0JQJ7BW3RPD0EB5MPjyBQKvLUHQevO++giDEkGRoDqMr5GMr5GMr5GAztdI/gI6jFRw/0RgDAAAAABJRU5ErkJggg==",T="data:image/png;base64,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",M="/assets/interface3-2a7d9ac4.png",U="/assets/interface4-6491aa95.png",N="/assets/interface5-fd8930fa.png",K={class:"markdown-body"},V=A("p",null,"物件導向程式設計期末專題。",-1),J=A("p",null,[s("這是以 "),A("a",{href:"http://zty.pe/"},"ZType"),s(" 作為靈感來源而開發的打字射擊遊戲,以 Visual Studio 2015 建立專案,基於 "),A("a",{href:"http://www.cc.ntut.edu.tw/~wkchen/game/"},"Game Framework 4.8"),s(" 以 C++ 語言撰寫的 Windows 平台遊戲。")],-1),S=A("p",null,[A("img",{src:n,alt:"主選單"})],-1),j={id:"%E8%A9%A6%E7%8E%A9%E5%BD%B1%E7%89%87",tabindex:"-1"},Z=s("試玩影片 "),u={class:"header-anchor",href:"#%E8%A9%A6%E7%8E%A9%E5%BD%B1%E7%89%87"},I=A("div",{class:"embed-responsive"},[A("iframe",{class:"embed-responsive-item",src:"https://www.youtube.com/embed/j8L_ViHDzMY",allowfullscreen:""})],-1),O={id:"%E7%B0%A1%E4%BB%8B",tabindex:"-1"},f=s("簡介 "),P={class:"header-anchor",href:"#%E7%B0%A1%E4%BB%8B"},G={id:"%E7%8E%A9%E6%B3%95",tabindex:"-1"},H=s("玩法 "),X={class:"header-anchor",href:"#%E7%8E%A9%E6%B3%95"},v=A("p",null,[s("在本遊戲中有各種不同外貌的敵人(Enemy),但是他們都有一個共同點,那就是他們的身上都帶著一組英文單字。玩家必須在敵人接近之前,輸入其身上的單字,才能成功的把的敵人消滅。"),A("br"),A("img",{src:h,alt:""}),A("br"),A("img",{src:r,alt:""}),A("br"),A("img",{src:c,alt:""})],-1),w={id:"%E8%A6%8F%E5%89%87",tabindex:"-1"},z=s("規則 "),q={class:"header-anchor",href:"#%E8%A6%8F%E5%89%87"},_=A("p",null,"本遊戲除了普通的敵人外,另外還有多種強大的Boss,除了字數較長外,還各自擁有不同的技能。",-1),$={id:"%E7%89%B9%E6%AE%8A%E5%8A%9F%E8%83%BD",tabindex:"-1"},AA=s("特殊功能 "),sA={class:"header-anchor",href:"#%E7%89%B9%E6%AE%8A%E5%8A%9F%E8%83%BD"},tA=A("p",null,'當身邊有太多敵人即將接近,而且來不及將他們消滅的時候,玩家在每場遊戲,有三次的機會可以使用技能。按下"Enter"釋放出電磁脈衝(EMP),快速的將身邊的敵人消滅。',-1),oA={id:"%E5%9C%96%E5%BD%A2%2F%E7%BE%8E%E8%A1%93",tabindex:"-1"},aA=s("圖形/美術 "),iA={class:"header-anchor",href:"#%E5%9C%96%E5%BD%A2%2F%E7%BE%8E%E8%A1%93"},gA=A("p",null,[s("我們遊戲圖形是以8 BITS復古風作為設計風格,它最大的特色就是在圖形的邊緣,看起來會有明顯的鋸齒。為了達成像素畫(Pixel Art) 的美術風格,大部分的圖形我們都是利用"),A("strong",null,"小畫家"),s("來繪製。和一般的繪圖軟體相比,小畫家非常適合來製作點陣圖,因為他可以很精確的在每一格中填色,並且可以很輕易的輸出Bmp格式的圖片,正好符合我們的需求。")],-1),eA={id:"%E8%AA%AA%E6%98%8E%E7%95%AB%E9%9D%A2",tabindex:"-1"},EA=s("說明畫面 "),BA={class:"header-anchor",href:"#%E8%AA%AA%E6%98%8E%E7%95%AB%E9%9D%A2"},nA=a('
',1),hA={id:"%E8%A7%92%E8%89%B2%E9%81%B8%E6%93%87",tabindex:"-1"},rA=s("角色選擇 "),cA={class:"header-anchor",href:"#%E8%A7%92%E8%89%B2%E9%81%B8%E6%93%87"},DA=A("p",null,[A("img",{src:L,alt:""}),A("br"),A("img",{src:F,alt:""}),A("br"),A("img",{src:Q,alt:""})],-1),pA={id:"%E9%81%8A%E6%88%B2%E7%95%AB%E9%9D%A2",tabindex:"-1"},mA=s("遊戲畫面 "),bA={class:"header-anchor",href:"#%E9%81%8A%E6%88%B2%E7%95%AB%E9%9D%A2"},lA=a('
',1),dA={id:"%E5%85%B6%E4%BB%96%E4%BB%8B%E9%9D%A2",tabindex:"-1"},WA=s("其他介面 "),LA={class:"header-anchor",href:"#%E5%85%B6%E4%BB%96%E4%BB%8B%E9%9D%A2"},FA=a('
',1),QA={id:"%E9%81%8A%E6%88%B2%E4%B8%8B%E8%BC%89",tabindex:"-1"},xA=s("遊戲下載 "),RA={class:"header-anchor",href:"#%E9%81%8A%E6%88%B2%E4%B8%8B%E8%BC%89"},CA=A("p",null,[A("a",{href:"/files/TypingTyping.zip"},"TypingTyping.zip (7.7mb, Windows only)")],-1),YA=A("iframe",{src:"https://ghbtns.com/github-btn.html?user=ngseke&repo=Typing-Typing&type=star&count=false",frameborder:"0",scrolling:"0",width:"150",height:"20"},null,-1),UA={title:"Typing Typing!",briefDescription:"8-bit 復古風格打字遊戲",githubLink:"https://github.com/ngseke/Typing-Typing",period:["2017/02","2017/06"],members:["余鎧企","黃省喬"],cover:"/img/project-cover/typing-typing.png",tags:["C++"]},NA="",KA=g({__name:"typing-typing",setup(kA,{expose:i}){return i({frontmatter:{title:"Typing Typing!",briefDescription:"8-bit 復古風格打字遊戲",githubLink:"https://github.com/ngseke/Typing-Typing",period:["2017/02","2017/06"],members:["余鎧企","黃省喬"],cover:"/img/project-cover/typing-typing.png",tags:["C++"]},excerpt:void 0}),(yA,TA)=>{const t=e("icon-link");return E(),B("div",K,[V,J,S,A("h2",j,[Z,A("a",u,[o(t)])]),I,A("h2",O,[f,A("a",P,[o(t)])]),A("h3",G,[H,A("a",X,[o(t)])]),v,A("h3",w,[z,A("a",q,[o(t)])]),_,A("h3",$,[AA,A("a",sA,[o(t)])]),tA,A("h3",oA,[aA,A("a",iA,[o(t)])]),gA,A("h2",eA,[EA,A("a",BA,[o(t)])]),nA,A("h2",hA,[rA,A("a",cA,[o(t)])]),DA,A("h2",pA,[mA,A("a",bA,[o(t)])]),lA,A("h2",dA,[WA,A("a",LA,[o(t)])]),FA,A("h2",QA,[xA,A("a",RA,[o(t)])]),CA,YA])}}});export{KA as default,NA as excerpt,UA as frontmatter};
diff --git a/assets/typingtyping-b132256f.js b/assets/typingtyping-b132256f.js
new file mode 100644
index 00000000..f0f7fe5e
--- /dev/null
+++ b/assets/typingtyping-b132256f.js
@@ -0,0 +1 @@
+import{m as o}from"./app-033a95d9.js";const f={};typeof o=="function"&&o(f);export{f as default};
diff --git a/assets/useOgImage-203f60bd.js b/assets/useOgImage-203f60bd.js
new file mode 100644
index 00000000..1de06da0
--- /dev/null
+++ b/assets/useOgImage-203f60bd.js
@@ -0,0 +1,18 @@
+import{C as le,I as Ne,J as $e,K as Be,L as se,M as ce,N as Pe,d as R,o as x,a as S,h as _,r as Z,O as Y,u as B,_ as G,j as U,w as ue,F as Re,i as je,t as N,b as $,q as H,P as Ae,Q as Me,R as qe,S as Ue,U as Ve,W as oe,X as me,D as Ye,c as We,k as fe,Y as De,H as Ke,Z as Ze,$ as Fe,a0 as Ge,p as Xe,a1 as Je,a2 as Qe}from"./app-033a95d9.js";/*! medium-zoom 1.0.6 | MIT License | https://github.com/francoischalifour/medium-zoom */var L=Object.assign||function(e){for(var t=1;t1&&arguments[1]!==void 0?arguments[1]:{},a=window.Promise||function(r){function d(){}r(d,d)},i=function(r){var d=r.target;if(d===A){E();return}y.indexOf(d)!==-1&&J({target:d})},m=function(){if(!(T||!n.original)){var r=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0;Math.abs(Q-r)>s.scrollOffset&&setTimeout(E,150)}},z=function(r){var d=r.key||r.keyCode;(d==="Escape"||d==="Esc"||d===27)&&E()},c=function(){var r=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},d=r;if(r.background&&(A.style.background=r.background),r.container&&r.container instanceof Object&&(d.container=L({},s.container,r.container)),r.template){var u=V(r.template)?r.template:document.querySelector(r.template);d.template=u}return s=L({},s,d),y.forEach(function(g){g.dispatchEvent(I("medium-zoom:update",{detail:{zoom:v}}))}),v},f=function(){var r=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};return e(L({},s,r))},w=function(){for(var r=arguments.length,d=Array(r),u=0;u0?d.reduce(function(l,h){return[].concat(l,re(h))},[]):y;return g.forEach(function(l){l.classList.remove("medium-zoom-image"),l.dispatchEvent(I("medium-zoom:detach",{detail:{zoom:v}}))}),y=y.filter(function(l){return g.indexOf(l)===-1}),v},he=function(r,d){var u=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};return y.forEach(function(g){g.addEventListener("medium-zoom:"+r,d,u)}),j.push({type:"medium-zoom:"+r,listener:d,options:u}),v},_e=function(r,d){var u=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};return y.forEach(function(g){g.removeEventListener("medium-zoom:"+r,d,u)}),j=j.filter(function(g){return!(g.type==="medium-zoom:"+r&&g.listener.toString()===d.toString())}),v},X=function(){var r=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},d=r.target,u=function(){var l={width:document.documentElement.clientWidth,height:document.documentElement.clientHeight,left:0,top:0,right:0,bottom:0},h=void 0,b=void 0;if(s.container)if(s.container instanceof Object)l=L({},l,s.container),h=l.width-l.left-l.right-s.margin*2,b=l.height-l.top-l.bottom-s.margin*2;else{var C=V(s.container)?s.container:document.querySelector(s.container),O=C.getBoundingClientRect(),D=O.width,Ee=O.height,xe=O.left,we=O.top;l=L({},l,{width:D,height:Ee,left:xe,top:we})}h=h||l.width-s.margin*2,b=b||l.height-s.margin*2;var k=n.zoomedHd||n.original,Se=ae(k)?h:k.naturalWidth||h,Oe=ae(k)?b:k.naturalHeight||b,M=k.getBoundingClientRect(),Le=M.top,Te=M.left,ee=M.width,te=M.height,Ce=Math.min(Se,h)/ee,Ie=Math.min(Oe,b)/te,K=Math.min(Ce,Ie),He=(-Te+(h-ee)/2+s.margin+l.left)/K,ke=(-Le+(b-te)/2+s.margin+l.top)/K,ne="scale("+K+") translate3d("+He+"px, "+ke+"px, 0)";n.zoomed.style.transform=ne,n.zoomedHd&&(n.zoomedHd.style.transform=ne)};return new a(function(g){if(d&&y.indexOf(d)===-1){g(v);return}var l=function D(){T=!1,n.zoomed.removeEventListener("transitionend",D),n.original.dispatchEvent(I("medium-zoom:opened",{detail:{zoom:v}})),g(v)};if(n.zoomed){g(v);return}if(d)n.original=d;else if(y.length>0){var h=y;n.original=h[0]}else{g(v);return}if(n.original.dispatchEvent(I("medium-zoom:open",{detail:{zoom:v}})),Q=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,T=!0,n.zoomed=nt(n.original),document.body.appendChild(A),s.template){var b=V(s.template)?s.template:document.querySelector(s.template);n.template=document.createElement("div"),n.template.appendChild(b.content.cloneNode(!0)),document.body.appendChild(n.template)}if(document.body.appendChild(n.zoomed),window.requestAnimationFrame(function(){document.body.classList.add("medium-zoom--opened")}),n.original.classList.add("medium-zoom-image--hidden"),n.zoomed.classList.add("medium-zoom-image--opened"),n.zoomed.addEventListener("click",E),n.zoomed.addEventListener("transitionend",l),n.original.getAttribute("data-zoom-src")){n.zoomedHd=n.zoomed.cloneNode(),n.zoomedHd.removeAttribute("srcset"),n.zoomedHd.removeAttribute("sizes"),n.zoomedHd.src=n.zoomed.getAttribute("data-zoom-src"),n.zoomedHd.onerror=function(){clearInterval(C),console.warn("Unable to reach the zoom image target "+n.zoomedHd.src),n.zoomedHd=null,u()};var C=setInterval(function(){n.zoomedHd.complete&&(clearInterval(C),n.zoomedHd.classList.add("medium-zoom-image--opened"),n.zoomedHd.addEventListener("click",E),document.body.appendChild(n.zoomedHd),u())},10)}else if(n.original.hasAttribute("srcset")){n.zoomedHd=n.zoomed.cloneNode(),n.zoomedHd.removeAttribute("sizes"),n.zoomedHd.removeAttribute("loading");var O=n.zoomedHd.addEventListener("load",function(){n.zoomedHd.removeEventListener("load",O),n.zoomedHd.classList.add("medium-zoom-image--opened"),n.zoomedHd.addEventListener("click",E),document.body.appendChild(n.zoomedHd),u()})}else u()})},E=function(){return new a(function(r){if(T||!n.original){r(v);return}var d=function u(){n.original.classList.remove("medium-zoom-image--hidden"),document.body.removeChild(n.zoomed),n.zoomedHd&&document.body.removeChild(n.zoomedHd),document.body.removeChild(A),n.zoomed.classList.remove("medium-zoom-image--opened"),n.template&&document.body.removeChild(n.template),T=!1,n.zoomed.removeEventListener("transitionend",u),n.original.dispatchEvent(I("medium-zoom:closed",{detail:{zoom:v}})),n.original=null,n.zoomed=null,n.zoomedHd=null,n.template=null,r(v)};T=!0,document.body.classList.remove("medium-zoom--opened"),n.zoomed.style.transform="",n.zoomedHd&&(n.zoomedHd.style.transform=""),n.template&&(n.template.style.transition="opacity 150ms",n.template.style.opacity=0),n.original.dispatchEvent(I("medium-zoom:close",{detail:{zoom:v}})),n.zoomed.addEventListener("transitionend",d)})},J=function(){var r=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},d=r.target;return n.original?E():X({target:d})},ze=function(){return s},ye=function(){return y},be=function(){return n.original},y=[],j=[],T=!1,Q=0,s=o,n={original:null,zoomed:null,zoomedHd:null,template:null};Object.prototype.toString.call(t)==="[object Object]"?s=t:(t||typeof t=="string")&&w(t),s=L({margin:0,background:"#fff",scrollOffset:40,container:null,template:null},s);var A=tt(s.background);document.addEventListener("click",i),document.addEventListener("keyup",z),document.addEventListener("scroll",m),window.addEventListener("resize",E);var v={open:X,close:E,toggle:J,update:c,clone:f,attach:w,detach:W,on:he,off:_e,getOptions:ze,getImages:ye,getZoomedImage:be};return v};function at(e,t){t===void 0&&(t={});var o=t.insertAt;if(!(!e||typeof document>"u")){var a=document.head||document.getElementsByTagName("head")[0],i=document.createElement("style");i.type="text/css",o==="top"&&a.firstChild?a.insertBefore(i,a.firstChild):a.appendChild(i),i.styleSheet?i.styleSheet.cssText=e:i.appendChild(document.createTextNode(e))}}var rt=".medium-zoom-overlay{position:fixed;top:0;right:0;bottom:0;left:0;opacity:0;transition:opacity .3s;will-change:opacity}.medium-zoom--opened .medium-zoom-overlay{cursor:pointer;cursor:zoom-out;opacity:1}.medium-zoom-image{cursor:pointer;cursor:zoom-in;transition:transform .3s cubic-bezier(.2,0,.2,1)!important}.medium-zoom-image--hidden{visibility:hidden}.medium-zoom-image--opened{position:relative;cursor:pointer;cursor:zoom-out;will-change:transform}";at(rt);const it=ot;function dt(e="*:not(a) > img"){le(()=>{if(!Ne)return;const t=it(e,{margin:16});$e(()=>t.detach()),Be(ce(),async o=>{await se(),t.update({background:o?"#1B1917":"#fff"})},{immediate:!0})}),Pe(`
+ .medium-zoom-overlay {
+ z-index: 100;
+ }
+
+ .medium-zoom-image.medium-zoom-image--opened{
+ z-index: 101;
+ }
+ `)}function lt(){le(async()=>{var t;await se();const{hash:e}=location;if(e){const o=e.split("#")[1];(t=document.getElementById(o))==null||t.scrollIntoView()}})}const st=R({__name:"Prose",setup(e){dt(),lt();const t=ce();return(o,a)=>(x(),S("div",null,[_("article",{class:Y(["prose",{dark:B(t)}])},[Z(o.$slots,"default",{},void 0,!0)],2)]))}});const ct=G(st,[["__scopeId","data-v-eb8991d3"]]),ut={},mt={class:"container px-4 py-8 md:pt-16"},ft={class:"mx-auto max-w-3xl pt-10 md:pt-16"},gt={class:"mb-12 border-b border-black-300 pb-4 dark:border-black-700"};function vt(e,t){const o=ct;return x(),S("div",mt,[_("div",ft,[_("header",gt,[Z(e.$slots,"header")]),_("main",null,[U(o,null,{default:ue(()=>[Z(e.$slots,"default")]),_:3})])])])}const At=G(ut,[["render",vt]]),pt=R({__name:"ProjectTags",props:{list:{default:void 0},size:{default:"md"}},setup(e){return(t,o)=>e.list?(x(),S("ul",{key:0,class:Y(["mb-2 flex font-medium",{"space-x-1 text-sm":e.size==="md","space-x-2 text-xl":e.size==="lg"}])},[(x(!0),S(Re,null,je(e.list,(a,i)=>(x(),S("li",{key:i,class:Y(["rounded-md bg-black-400/20",{"px-1":e.size==="md","px-2 py-1":e.size==="lg"}])},N(a),3))),128))],2)):$("",!0)}});let ge=(e=21)=>crypto.getRandomValues(new Uint8Array(e)).reduce((t,o)=>(o&=63,o<36?t+=o.toString(36):o<62?t+=(o-26).toString(36).toUpperCase():o>62?t+="-":t+="_",t),"");var ie={created(e,t){const[o,a]=t.value;e.setAttribute(o,a||ge(5))},getSSRProps(e){const[t,o]=e.value;return{[t]:o}}},F="__wrap_b",P="__wrap_n",de="__wrap_o",ve=(e,t,o)=>{o=o||document.querySelector(`[data-br="${e}"]`);const a=o.parentElement,i=W=>o.style.maxWidth=`${W}px`;o.style.maxWidth="";const m=a.clientWidth,z=a.clientHeight;let c=m/2-.25,f=m+.5,w;if(m){for(i(c),c=Math.max(o.scrollWidth,c);c+1{self.__wrap_b(0,+o.dataset.brr,o)})).observe(a)},ht=ve.toString(),_t='(self.CSS&&CSS.supports("text-wrap","balance")?1:2)';function pe(e,t,o){return o&&(o=`self.${P}!=1&&${o}`),me("script",{innerHTML:(e?"":`self.${P}=self.${P}||${_t};self.${F}=${ht};`)+o,nonce:t})}R({name:"BalancerProvider",props:{preferNative:{type:Boolean,required:!1,default:!0},nonce:{type:String,required:!1}},setup(e,{slots:t}){const o=H(()=>e.preferNative);return Ae("BALANCER_PROVIDER",{preferNative:o,hasProvider:!0}),()=>{var a;return[pe(!1,e.nonce),(a=t.default)==null?void 0:a.call(t)]}}});var zt=R({name:"WrapBalancer",props:{as:{type:String,required:!1,default:"span"},ratio:{type:Number,required:!1,default:1},preferNative:{type:Boolean,required:!1,default:!0},nonce:{type:String,required:!1}},setup(e,{slots:t,attrs:o}){const a=e.as,i=o.id||ge(5),m=Me(null),z=qe("BALANCER_PROVIDER",{preferNative:!0,hasProvider:!1}),c=H(()=>{var f;return(f=e.preferNative)!=null?f:B(z.preferNative)});return Ue(()=>{c.value&&typeof self<"u"&&self[P]===1||m.value&&(self[F]=ve)(0,e.ratio,m.value)}),Ve(()=>{if(c.value&&typeof self<"u"&&self[P]===1||!m.value)return;const f=m.value[de];f&&(f.disconnect(),delete m.value[de])}),()=>{var f;return oe(me(a,{...o,"data-brr":e.ratio,ref:m,style:{...o.style,display:"inline-block",verticalAlign:"top",textDecoration:"inherit",textWrap:c.value?"balance":"initial"}},[(f=t.default)==null?void 0:f.call(t),oe(pe(z.hasProvider,e.nonce,`self.${F}(document.currentScript.dataset.ssrId,${e.ratio})`),[[ie,["data-ssr-id",i]]])]),[[ie,["data-br",i]]])}}});/*!
+ * Original code by Shu Ding
+ * MIT Licensed, Copyright 2022 Shu Ding, see https://github.com/shuding/react-wrap-balancer/blob/main/LICENSE.md for details
+ *
+ * Credits to the team:
+ * https://github.com/shuding/react-wrap-balancer/blob/main/src/index.tsx
+ */const yt=e=>(Fe("data-v-7d4dce4c"),e=e(),Ge(),e),bt={class:"absolute left-0 top-0 z-50 flex h-full max-h-[600px] w-full max-w-[1200px] border-b-[1rem] border-ngsek bg-black-900 px-20 pb-14 pt-10"},Et={class:"flex h-full w-full flex-col flex-wrap font-medium text-white"},xt={class:"flex flex-1 flex-wrap items-center"},wt={class:"flex h-full grow flex-col justify-center gap-3"},St={key:0,class:"text-4xl text-black-400"},Ot={class:"text-6xl font-bold leading-tight tracking-[1px]"},Lt={key:1,class:"text-2xl tracking-wider"},Tt={key:2,class:"mt-5"},Ct={key:0,class:"flex flex-1 justify-end"},It={class:"flex w-full justify-between"},Ht={class:"flex items-center gap-3 text-4xl text-black-400"},kt=fe(" By "),Nt=yt(()=>_("div",null,[_("div",{class:"neon inline-block rotate-[-4deg] font-pacifico text-4xl"}," ngseke ")],-1)),$t=R({__name:"OgImageTemplate",props:{title:null,description:null,date:null,tags:null,img:null},setup(e){const t=e,o=H(()=>Ye(t.date)),a=Ke;return(i,m)=>{var f;const z=pt,c=Ze;return x(),We(De,{to:"body"},[_("div",bt,[_("div",Et,[_("div",xt,[_("div",{class:Y(["flex",e.img?"w-3/5":"w-4/5"])},[_("div",wt,[e.date?(x(),S("div",St,N(B(o)),1)):$("",!0),_("h1",Ot,[U(B(zt),{ratio:.5},{default:ue(()=>[fe(N(e.title),1)]),_:1},8,["ratio"])]),e.description?(x(),S("p",Lt,N(e.description),1)):$("",!0),(f=e.tags)!=null&&f.length?(x(),S("div",Tt,[U(z,{list:e.tags,size:"lg"},null,8,["list"])])):$("",!0)])],2),e.img?(x(),S("div",Ct,[U(c,{img:e.img,sizeClassName:"h-[12.1rem] w-[17.6rem]"},null,8,["img"])])):$("",!0)]),_("div",It,[_("div",Ht,[kt,_("span",null,N(B(a)),1)]),Nt])])])])}}});const Mt=G($t,[["__scopeId","data-v-7d4dce4c"]]);function Bt(e){var t=[];if(e.length===0)return"";if(typeof e[0]!="string")throw new TypeError("Url must be a string. Received "+e[0]);if(e[0].match(/^[^/:]+:\/*$/)&&e.length>1){var o=e.shift();e[0]=o+e[0]}e[0].match(/^file:\/\/\//)?e[0]=e[0].replace(/^([^/:]+):\/*/,"$1:///"):e[0]=e[0].replace(/^([^/:]+):\/*/,"$1://");for(var a=0;a0&&(i=i.replace(/^[\/]+/,"")),a0?"?":"")+z.join("&"),m}function Pt(){var e;return typeof arguments[0]=="object"?e=arguments[0]:e=[].slice.call(arguments),Bt(e)}const Rt="og-image";function qt(){const e=Xe(),t=H(()=>{const i=e.name;return typeof i!="string"?null:Pt(Je,Qe,`${i}.png`,`?v=b795911
+`).trim()}),o=H(()=>t.value?[{property:"og:image",content:t.value},{property:"og:image:width",content:1200},{property:"og:image:height",content:600},{name:"twitter:image",content:t.value},{name:"twitter:card",content:"summary_large_image"}]:[]),a=H(()=>Rt in e.query);return{ogImage:t,ogImageHeadMetaList:o,shouldShowOgImage:a}}export{Mt as _,pt as a,At as b,qt as u};
diff --git a/assets/useOgImage-588433c9.css b/assets/useOgImage-588433c9.css
new file mode 100644
index 00000000..04032c30
--- /dev/null
+++ b/assets/useOgImage-588433c9.css
@@ -0,0 +1 @@
+[data-v-eb8991d3] .prose h2,[data-v-eb8991d3] .prose h3,[data-v-eb8991d3] .prose h4{margin-top:3rem;margin-bottom:1rem;font-weight:500}[data-v-eb8991d3] .prose h2:before,[data-v-eb8991d3] .prose h3:before,[data-v-eb8991d3] .prose h4:before{pointer-events:none;margin-top:-5rem;display:block;height:5rem;--tw-content: "";content:var(--tw-content)}[data-v-eb8991d3] .prose h2 a.header-anchor,[data-v-eb8991d3] .prose h3 a.header-anchor,[data-v-eb8991d3] .prose h4 a.header-anchor{margin-left:.25rem;margin-top:-.5rem;display:inline-block;--tw-scale-x: .9;--tw-scale-y: .9;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));vertical-align:middle;font-size:.875rem;line-height:1.25rem;opacity:0;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}[data-v-eb8991d3] .prose h2:hover .header-anchor,[data-v-eb8991d3] .prose h3:hover .header-anchor,[data-v-eb8991d3] .prose h4:hover .header-anchor{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));opacity:.8}[data-v-eb8991d3] .prose h2{font-size:1.875rem;line-height:2.25rem}[data-v-eb8991d3] .prose h3{font-size:1.5rem;line-height:2rem}[data-v-eb8991d3] .prose h4{font-size:1.25rem;line-height:1.75rem}[data-v-eb8991d3] .prose p{margin-top:1.5rem;margin-bottom:1rem;line-height:1.75rem;letter-spacing:.2px}[data-v-eb8991d3] .prose p>img{margin-top:1.5rem;margin-bottom:1rem;max-width:min(30rem,100%);margin-left:auto;margin-right:auto;width:auto}[data-v-eb8991d3] .prose blockquote{border-left-width:4px;border-color:#73737380;padding-left:1rem;padding-right:1rem;--tw-text-opacity: 1;color:rgb(82 82 82 / var(--tw-text-opacity))}[data-v-eb8991d3] .prose blockquote p img{margin-left:0;margin-right:0;max-width:100%}[data-v-eb8991d3] .prose ul,[data-v-eb8991d3] .prose ol{margin-top:1.5rem;margin-bottom:1rem;list-style-position:outside}[data-v-eb8991d3] .prose ul>:not([hidden])~:not([hidden]),[data-v-eb8991d3] .prose ol>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}[data-v-eb8991d3] .prose ul,[data-v-eb8991d3] .prose ol{padding-left:1.25rem}[data-v-eb8991d3] .prose ul li,[data-v-eb8991d3] .prose ol li{padding-left:.5rem}[data-v-eb8991d3] .prose ul{list-style-type:disc}[data-v-eb8991d3] .prose ol{list-style-type:decimal}[data-v-eb8991d3] .prose a{text-decoration-line:underline;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}@media (hover: hover) and (pointer: fine){[data-v-eb8991d3] .prose a:hover{text-decoration-color:#ffd01980}}[data-v-eb8991d3] .prose s,[data-v-eb8991d3] .prose s>a{text-decoration-line:line-through}[data-v-eb8991d3] .prose hr{margin-top:1.5rem;margin-bottom:1.5rem;border-color:currentColor;opacity:.3}[data-v-eb8991d3] .prose a,[data-v-eb8991d3] .prose code{overflow-wrap:break-word}[data-v-eb8991d3] .prose :not(pre)>code{border-radius:.375rem;background-color:#a3a3a333;padding-left:.4rem;padding-right:.4rem}[data-v-eb8991d3] .prose .embed-responsive{margin-top:1.5rem;margin-bottom:1rem;max-width:min(30rem,100%);position:relative;margin-left:auto;margin-right:auto;width:100%;overflow:hidden}[data-v-eb8991d3] .prose .embed-responsive:after{display:block;content:var(--tw-content);padding-top:56.25%}[data-v-eb8991d3] .prose .embed-responsive .embed-responsive-item{position:absolute;top:0;right:0;bottom:0;left:0;height:100%;width:100%}[data-v-eb8991d3] .prose .shiki{margin-top:1.5rem;margin-bottom:1rem;overflow-x:auto;border-radius:.5rem;padding:1rem}[data-v-eb8991d3] .prose .shiki-light{--tw-bg-opacity: 1 !important;background-color:rgb(245 245 245 / var(--tw-bg-opacity))!important}[data-v-eb8991d3] .prose .shiki-dark{display:none}[data-v-eb8991d3] .prose.dark blockquote{--tw-text-opacity: 1;color:rgb(163 163 163 / var(--tw-text-opacity))}[data-v-eb8991d3] .prose.dark .shiki-light{display:none}[data-v-eb8991d3] .prose.dark .shiki-dark{display:block}.neon[data-v-7d4dce4c]{color:#fff;text-shadow:0 0 6px rgba(255,255,255,.92),0 0 30px rgba(255,255,255,.34),0 0 12px rgba(255,208,25,.52),0 0 21px rgba(255,208,25,.5),0 0 34px rgba(255,208,25,.7)}
diff --git a/assets/useReadHistory-d98c4b5d.js b/assets/useReadHistory-d98c4b5d.js
new file mode 100644
index 00000000..530cfca9
--- /dev/null
+++ b/assets/useReadHistory-d98c4b5d.js
@@ -0,0 +1 @@
+import{z as r,f as c,q as i}from"./app-033a95d9.js";const u="readHistory";function y(){const e=r(u,{}),t=s=>{s in e||(e.value[s]=+new Date)},{projectMap:o}=c(),a=i(()=>Object.keys(o.value).every(s=>s in e.value));return{readHistory:e,pushReadHistory:t,isReadAll:a,clearReadHistory:()=>{e.value={}}}}export{y as u};
diff --git a/assets/valueOf-956b83dd.png b/assets/valueOf-956b83dd.png
new file mode 100644
index 00000000..d168b030
Binary files /dev/null and b/assets/valueOf-956b83dd.png differ
diff --git a/assets/versatile-npm-d3f5c477.js b/assets/versatile-npm-d3f5c477.js
new file mode 100644
index 00000000..51528eb4
--- /dev/null
+++ b/assets/versatile-npm-d3f5c477.js
@@ -0,0 +1 @@
+import{d as a,v as r,o as i,a as p,h as e,j as t,k as s,A as c}from"./app-033a95d9.js";const h="/assets/1400x560-02b6cc6e.png",l="/assets/demo-222ae85f.gif",d="/assets/screenshot-2-9726f1a7.png",m="/assets/screenshot-1-81b8fb35.png",g="/assets/logo-candidates-4f73c7bd.png",_={class:"markdown-body"},b=e("p",null,[e("img",{src:h,alt:""})],-1),f={id:"%E7%B0%A1%E4%BB%8B",tabindex:"-1"},u=s("簡介 "),k={class:"header-anchor",href:"#%E7%B0%A1%E4%BB%8B"},v=c('Versatile Npm 是 Google Chrome 擴充功能,使用 Vue 3 + TypeScript + Chrome Extension API + Vuetify 建構。
這個擴充功能可以讓使用者在 npm 套件頁面的右側欄加入 yarn
和 pnpm
等安裝指令,方便開發者一鍵複製。
另外也提供設定 UI 供使用者自定義安裝指令,執行新增、刪除、編輯和排序操作。
',6),j={id:"logo",tabindex:"-1"},V=s("Logo "),y={class:"header-anchor",href:"#logo"},B=c('這款 App 的 Logo 是透過 DALL·E 3 產生的,輸入 prompt 基本上包含 穿著帽 T 的水豚
、包裹
、紅色
、badge style
、sport mascot style
,最後透過 macOS 內建的「移除背景」功能去背。這過程幾乎不費吹灰之力,完全無需任何專業繪圖軟體。
其他候選 Logo 如下:
',2),x={id:"demo",tabindex:"-1"},L=s("Demo "),N={class:"header-anchor",href:"#demo"},T=e("a",{href:"https://chromewebstore.google.com/detail/versatile-npm/jahejogdoffpehfhkhbpjblnlhghjnje?hl=zh-TW",target:"_blank"},[e("img",{src:"https://storage.googleapis.com/web-dev-uploads/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png"})],-1),S=e("br",null,null,-1),w=e("iframe",{src:"https://ghbtns.com/github-btn.html?user=ngseke&repo=versatile-npm&type=star&count=false",frameborder:"0",scrolling:"0",width:"150",height:"20"},null,-1),W={title:"Versatile Npm",briefDescription:"自訂 Npm 安裝指令瀏覽器擴充功能",githubLink:"https://github.com/ngseke/versatile-npm",demoLink:"https://chromewebstore.google.com/detail/versatile-npm/jahejogdoffpehfhkhbpjblnlhghjnje?hl=zh-TW",period:["2023/11","2023/11"],cover:"/img/project-cover/versatile-npm.png",tags:["Vue","TypeScript","Vuetify","npm"]},U="",z=a({__name:"versatile-npm",setup(C,{expose:n}){return n({frontmatter:{title:"Versatile Npm",briefDescription:"自訂 Npm 安裝指令瀏覽器擴充功能",githubLink:"https://github.com/ngseke/versatile-npm",demoLink:"https://chromewebstore.google.com/detail/versatile-npm/jahejogdoffpehfhkhbpjblnlhghjnje?hl=zh-TW",period:["2023/11","2023/11"],cover:"/img/project-cover/versatile-npm.png",tags:["Vue","TypeScript","Vuetify","npm"]},excerpt:void 0}),(E,A)=>{const o=r("icon-link");return i(),p("div",_,[b,e("h2",f,[u,e("a",k,[t(o)])]),v,e("h2",j,[V,e("a",y,[t(o)])]),B,e("h2",x,[L,e("a",N,[t(o)])]),T,S,w])}}});export{z as default,U as excerpt,W as frontmatter};
diff --git a/assets/vite-vue-ts-eslint-setup-1bf5e98d.js b/assets/vite-vue-ts-eslint-setup-1bf5e98d.js
new file mode 100644
index 00000000..27fab800
--- /dev/null
+++ b/assets/vite-vue-ts-eslint-setup-1bf5e98d.js
@@ -0,0 +1,165 @@
+import{d as p,v as t,o as c,a as i,h as s,j as e,k as n,A as o}from"./app-033a95d9.js";const r="/assets/output-parserOptions-project-error-ceeaa787.png",d="/assets/output-parserOptions-project-without-error-74ff031b.png",u="/assets/tsconfig-include-a4dd9125.png",y="/assets/command-reload-window-955490ff.png",A="/assets/vscode-linting-result-f315bd6a.png",h="/assets/vue-file-error-73be3796.png",E="/assets/move-object-properties-99f2f466.gif",g="/assets/vscode-auto-fix-on-save-445e3f9d.gif",f="/assets/terminal-lint-09df797c.png",B="/assets/eslint-automatically-fixable-4b63f152.png",D={class:"markdown-body"},q=s("blockquote",null,[s("p",null,"以下使用 pnpm 作為套件管理工具,與 npm 或 yarn 的指令會有些許差異,請查看隨附的官方文件")],-1),_={id:"tl%3Bdr",tabindex:"-1"},k=n("TL;DR "),v={class:"header-anchor",href:"#tl%3Bdr"},C=s("p",null,[n("完整配置範例:"),s("a",{href:"https://github.com/ngseke/vite-vue-ts-eslint-example"},"https://github.com/ngseke/vite-vue-ts-eslint-example")],-1),m={id:"%F0%9F%8F%97%EF%B8%8F-%E9%80%8F%E9%81%8E-vite-%E5%AE%98%E6%96%B9%E7%9A%84-preset-%E6%96%B0%E5%BB%BA%E5%B0%88%E6%A1%88",tabindex:"-1"},F=n("🏗️ 透過 Vite 官方的 Preset 新建專案 "),b={class:"header-anchor",href:"#%F0%9F%8F%97%EF%B8%8F-%E9%80%8F%E9%81%8E-vite-%E5%AE%98%E6%96%B9%E7%9A%84-preset-%E6%96%B0%E5%BB%BA%E5%B0%88%E6%A1%88"},j=o(`Scaffolding Your First Vite Project
pnpm create vite my-vue-app --template vue-ts
+
pnpm create vite my-vue-app --template vue-ts
+
移動到該目錄並安裝相依
cd my-vue-app
+pnpm i
+
cd my-vue-app
+pnpm i
+
`,4),x={id:"%F0%9F%8F%97%EF%B8%8F-%E5%88%9D%E5%A7%8B%E5%8C%96-eslint",tabindex:"-1"},w=n("🏗️ 初始化 ESLint "),S={class:"header-anchor",href:"#%F0%9F%8F%97%EF%B8%8F-%E5%88%9D%E5%A7%8B%E5%8C%96-eslint"},V=o(`Getting Started with ESLint - Quick start
pnpm create @eslint/config
+
pnpm create @eslint/config
+
依序回答以下問題後,便會在根目錄新建 .eslintrc.cjs
:
✔ How would you like to use ESLint? · style ✔ What type of modules does your project use? · esm ✔ Which framework does your project use? · vue ✔ Does your project use TypeScript? · No / Yes ✔ Where does your code run? · browser ✔ How would you like to define a style for your project? · guide ✔ Which style guide do you want to follow? · standard-with-typescript ✔ What format do you want your config file to be in? · JavaScript ✔ Would you like to install them now? · No / Yes ✔ Which package manager do you want to use? · pnpm
`,4),O={id:"%F0%9F%98%B5-%E8%99%95%E7%90%86-vscode-output-%E7%9A%84-eslint-%E7%9A%84%E5%95%8F%E9%A1%8C",tabindex:"-1"},L=n("😵 處理 VSCode Output 的 ESLint 的問題 "),T={class:"header-anchor",href:"#%F0%9F%98%B5-%E8%99%95%E7%90%86-vscode-output-%E7%9A%84-eslint-%E7%9A%84%E5%95%8F%E9%A1%8C"},P=o('雖然 ESLint 已初始化完成,但你會發現它尚未正常運作。例如試著在任意 .ts
或 .vue
檔隨便加多餘的空格,卻看不到預期的 error 或 warning 的波浪底線。
查看 Output 會看到以下錯誤訊息:
An unexpected error occurred: Error: Error while loading rule ‘@typescript-eslint/dot-notation’: You have used a rule which requires parserServices to be generated. You must therefore provide a value for the “parserOptions.project” property for @typescript-eslint/parser.
根據錯誤訊息的描述,得知需要在 .eslintrc.cjs
補上 parserOptions.project
並指明 parserOptions.parser
。
// .eslintrc.cjs
+module.exports = {
+ // ...
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module",
++ project: ['./tsconfig.json', './tsconfig.node.json'],
++ parser: '@typescript-eslint/parser',
+ },
+}
+
// .eslintrc.cjs
+module.exports = {
+ // ...
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module",
++ project: ['./tsconfig.json', './tsconfig.node.json'],
++ parser: '@typescript-eslint/parser',
+ },
+}
+
再次查看 Output 錯誤訊息已消失
Q: 為什麼是 parserOptions.parser
而非 parser
? A: 根據官方文件 說明,若寫在 parser
會把 vue-eslint-parser
覆蓋掉而無法正常地 lint .vue
檔。所以當有自訂的 parser 時(例如 @typescript-eslint/parser
),必須把它移入 parserOptions
。
',9),W={id:"%F0%9F%98%B5-%E8%99%95%E7%90%86%E6%A0%B9%E7%9B%AE%E9%8C%84%E6%AA%94-tsconfig-include-%E7%9A%84%E5%95%8F%E9%A1%8C",tabindex:"-1"},N=n("😵 處理根目錄檔 TSConfig include 的問題 "),Y={class:"header-anchor",href:"#%F0%9F%98%B5-%E8%99%95%E7%90%86%E6%A0%B9%E7%9B%AE%E9%8C%84%E6%AA%94-tsconfig-include-%E7%9A%84%E5%95%8F%E9%A1%8C"},R=o('雖然 Output 錯誤訊息已消失,但這時用 VSCode 開啟在根目錄的 .ts
和 .cjs
檔,例如 vite.config.ts
和.eslintrc.cjs
,會發現它們在開頭都多了紅色的波浪底線:
Parsing error: ESLint was configured to run on <tsconfigRootDir>/.eslintrc.cjs
using parserOptions.project
: /users/sean/my-vue-app/tsconfig.json However, that TSConfig does not include this file. Either:
根據錯誤訊息得知,我們必須在 tsconfig.node.json
的 includes
中手動加入這些檔案:
// tsconfig.node.json
+{
+ // ...
+ "include": [
+ "vite.config.ts",
++ ".eslintrc.cjs"
+ ],
+}
+
// tsconfig.node.json
+{
+ // ...
+ "include": [
+ "vite.config.ts",
++ ".eslintrc.cjs"
+ ],
+}
+
日後若是在根目錄有新增那些在 Node 環境 執行的,而不會被實際打包進專案的設定檔,例如 Tailwind CSS 的設定檔 tailwind.config.ts
,也要記得手動加入進去。
接著重新載入 VSCode 視窗
叫出指令視窗 ⌘ + shift + P
→ 輸入 Developer: Reload Window
打開 .eslintrc.cjs
可以看到 ESLint 終於正常運作了,它開始確實根據 eslint-config-standard-with-typescript 的預設規則進行檢查,例如多餘的引號、字串應為單引號和縮排應為 2 格空格等。
',11),H={id:"%F0%9F%98%B5-%E8%99%95%E7%90%86-.vue-%E6%AA%94%E7%9A%84-non-standard-%E5%95%8F%E9%A1%8C",tabindex:"-1"},I=n("😵 處理 "),Q=s("code",{class:""},".vue",-1),G=n(" 檔的 non-standard 問題 "),J={class:"header-anchor",href:"#%F0%9F%98%B5-%E8%99%95%E7%90%86-.vue-%E6%AA%94%E7%9A%84-non-standard-%E5%95%8F%E9%A1%8C"},z=o('打開任意 .vue
檔會發現它們也出現了錯誤訊息:
Parsing error: ESLint was configured to run on <tsconfigRootDir>/src/App.vue
using parserOptions.project
: /users/sean/my-vue-app/tsconfig.json The extension for the file (.vue
) is non-standard. You should add parserOptions.extraFileExtensions
to your config.
根據錯誤訊息的提示在 .eslintrc.cjs
加入 parserOptions.extraFileExtensions
後,再次重新載入 VSCode 視窗即可。
// .eslintrc.cjs
+{
+ // ...
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module",
+ project: ['./tsconfig.json', './tsconfig.node.json'],
+ parser: '@typescript-eslint/parser',
++ extraFileExtensions: ['.vue']
+ },
+}
+
// .eslintrc.cjs
+{
+ // ...
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module",
+ project: ['./tsconfig.json', './tsconfig.node.json'],
+ parser: '@typescript-eslint/parser',
++ extraFileExtensions: ['.vue']
+ },
+}
+
`,5),K={id:"%F0%9F%98%B5-%E8%B7%B3%E9%81%8E%E6%AA%A2%E6%9F%A5%E6%9F%90%E4%BA%9B%E6%AA%94%E6%A1%88",tabindex:"-1"},M=n("😵 跳過檢查某些檔案 "),U={class:"header-anchor",href:"#%F0%9F%98%B5-%E8%B7%B3%E9%81%8E%E6%AA%A2%E6%9F%A5%E6%9F%90%E4%BA%9B%E6%AA%94%E6%A1%88"},X={id:"dist%2F",tabindex:"-1"},Z=s("code",{class:""},"dist/",-1),$=n(),ss={class:"header-anchor",href:"#dist%2F"},ns=o(`我們沒必要檢查建構好的已醜化和壓縮的檔案,因此可以在 .eslintrc.cjs
的 ignorePatterns
排除掉整個 dist
目錄:
// .eslintrc.cjs
+module.exports = {
+ // ...
++ ignorePatterns: ['dist'],
+}
+
// .eslintrc.cjs
+module.exports = {
+ // ...
++ ignorePatterns: ['dist'],
+}
+
`,2),as={id:"vite-env.d.ts",tabindex:"-1"},es=s("code",{class:""},"vite-env.d.ts",-1),os=n(),ls={class:"header-anchor",href:"#vite-env.d.ts"},ps=o(`按照預設規則,vite-env.d.ts
會違反 @typescript-eslint/triple-slash-reference
這條規則,建議可以在 vite-env.d.ts
的開頭加上註解 // eslint-disable-next-line ...
來跳過檢查:
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
+/// < reference types = "vite/client" />
+
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
+/// < reference types = "vite/client" />
+
`,2),ts={id:"%E2%9C%85-%E5%A4%A7%E5%8A%9F%E5%91%8A%E6%88%90",tabindex:"-1"},cs=n("✅ 大功告成 "),is={class:"header-anchor",href:"#%E2%9C%85-%E5%A4%A7%E5%8A%9F%E5%91%8A%E6%88%90"},rs=o(`到此為止 ESLint 應該就可以順利的運作,接著你可以更進一步根據個人或團隊的風格和偏好調整 rules
,開啟、關閉或調整某些規則。
例如我總是習慣微調 @typescript-eslint/comma-dangle (行末逗號)的設定,讓最後一項依然保留逗號,這樣就可以更方便調整物件成員的順序:
module . exports = {
+ // ...
+ rules : {
+ // ...
+ '@typescript-eslint/comma-dangle' : [ 'error' , {
+ arrays : 'always-multiline' ,
+ objects : 'always-multiline' ,
+ imports : 'always-multiline' ,
+ exports : 'always-multiline' ,
+ functions : 'only-multiline' ,
+ }],
+ },
+}
+
module . exports = {
+ // ...
+ rules : {
+ // ...
+ '@typescript-eslint/comma-dangle' : [ 'error' , {
+ arrays : 'always-multiline' ,
+ objects : 'always-multiline' ,
+ imports : 'always-multiline' ,
+ exports : 'always-multiline' ,
+ functions : 'only-multiline' ,
+ }],
+ },
+}
+
',4),ds={id:"%E2%9C%A8-%E8%A8%AD%E5%AE%9A-vscode-%E5%AD%98%E6%AA%94%E6%99%82%E8%87%AA%E5%8B%95%E6%8E%92%E7%89%88",tabindex:"-1"},us=n("✨ 設定 VSCode 存檔時自動排版 "),ys={class:"header-anchor",href:"#%E2%9C%A8-%E8%A8%AD%E5%AE%9A-vscode-%E5%AD%98%E6%AA%94%E6%99%82%E8%87%AA%E5%8B%95%E6%8E%92%E7%89%88"},As=o(`你還可以讓開發體驗變的更舒適。
打開 .vscode/settings.json
,若原本沒有這個檔案可以手動建立一個,加入以下設定:
{
+ // ...
+ "editor.codeActionsOnSave" : {
+ "source.fixAll.eslint" : true
+ },
+}
+
{
+ // ...
+ "editor.codeActionsOnSave" : {
+ "source.fixAll.eslint" : true
+ },
+}
+
回到剛才滿江紅的 .eslintrc.cjs
測試看看,在手動存檔(⌘ + S
)後就會自動排版和修正錯誤:
',5),hs={id:"%E2%9C%A8-%E9%80%8F%E9%81%8E-script-%E6%AA%A2%E6%9F%A5%E9%8C%AF%E8%AA%A4%E6%88%96%E6%98%AF%E8%87%AA%E5%8B%95%E4%BF%AE%E5%BE%A9%E9%8C%AF%E8%AA%A4",tabindex:"-1"},Es=n("✨ 透過 script 檢查錯誤或是自動修復錯誤 "),gs={class:"header-anchor",href:"#%E2%9C%A8-%E9%80%8F%E9%81%8E-script-%E6%AA%A2%E6%9F%A5%E9%8C%AF%E8%AA%A4%E6%88%96%E6%98%AF%E8%87%AA%E5%8B%95%E4%BF%AE%E5%BE%A9%E9%8C%AF%E8%AA%A4"},fs=s("p",null,[n("儘管在 VSCode 中的檢查和錯誤提示有助於得到即時反饋,但這些提示終究是 "),s("strong",null,"「消極的」"),n(" 。如果開發者使用別款 IDE,或只是單純對提示視若無睹,他們仍然可以輕易地違反這些規則,並提交不符合規則的程式碼。")],-1),Bs=s("p",null,"因此我們還需要透過 script 的方式來真正的執行規則檢查。",-1),Ds={id:"%E7%94%A8%E6%8C%87%E4%BB%A4%E6%AA%A2%E6%9F%A5%E9%8C%AF%E8%AA%A4",tabindex:"-1"},qs=n("用指令檢查錯誤 "),_s={class:"header-anchor",href:"#%E7%94%A8%E6%8C%87%E4%BB%A4%E6%AA%A2%E6%9F%A5%E9%8C%AF%E8%AA%A4"},ks=o(`打開 package.json
,在 scripts
中加入指令 lint
,其中 --ext
的後面是想要檢查的副檔名:
// package.json
+{
+ // ...
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc && vite build",
+ "preview": "vite preview",
++ "lint": "npx eslint . --ext .ts,.js,.cjs,.vue"
+ },
+}
+
// package.json
+{
+ // ...
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc && vite build",
+ "preview": "vite preview",
++ "lint": "npx eslint . --ext .ts,.js,.cjs,.vue"
+ },
+}
+
接著在 terminal 測試執行效果
pnpm run lint
+
pnpm run lint
+
可以看到它列出了所有不符合設定規則的 error 和 warning,並且回傳 exit code 1
。
這表示你就可以將這條指令整合進你的部署流程中,例如:
搭配 husky :使用 husky 可以在每次 commit 前自動執行 pnpm run lint
。如果程式碼不符合規則就不給 commit,迫使開發者修復錯誤後再進行 commit,確保 code base 的風格始終維持一致CI/CD :你可以將 pnpm run lint
加入 CI/CD 流程,這樣在每次部署前都會自動進行代碼檢查。若檢查失敗,部署流程就會自動中斷,讓開發者不得不修正錯誤 ',8),vs={id:"%E7%94%A8%E6%8C%87%E4%BB%A4%E8%87%AA%E5%8B%95%E4%BF%AE%E5%BE%A9%E9%8C%AF%E8%AA%A4",tabindex:"-1"},Cs=n("用指令自動修復錯誤 "),ms={class:"header-anchor",href:"#%E7%94%A8%E6%8C%87%E4%BB%A4%E8%87%AA%E5%8B%95%E4%BF%AE%E5%BE%A9%E9%8C%AF%E8%AA%A4"},Fs=o(`打開 package.json
,在 scripts
中加入以下指令 lint:fix
:
// package.json
+{
+ // ...
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc && vite build",
+ "preview": "vite preview",
+ "lint": "npx eslint . --ext .ts,.js,.cjs,.vue",
++ "lint:fix": "npx eslint --fix . --ext .ts,.js,.cjs,.vue"
+ },
+}
+
// package.json
+{
+ // ...
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc && vite build",
+ "preview": "vite preview",
+ "lint": "npx eslint . --ext .ts,.js,.cjs,.vue",
++ "lint:fix": "npx eslint --fix . --ext .ts,.js,.cjs,.vue"
+ },
+}
+
接著在 terminal 測試執行效果
pnpm run lint:fix
+
pnpm run lint:fix
+
順利的話「可自動修正 (automatically fixable)」的那些規則都會被自動修正,也就是在 Rule 列表 有 🔧 符號的那些項目。
',6),bs={id:"eslint-%E5%8D%B3%E5%B0%87%E6%A3%84%E7%94%A8%E3%80%8C%E6%8E%92%E7%89%88%E3%80%8D%E8%A6%8F%E5%89%87",tabindex:"-1"},js=n("ESLint 即將棄用「排版」規則 "),xs={class:"header-anchor",href:"#eslint-%E5%8D%B3%E5%B0%87%E6%A3%84%E7%94%A8%E3%80%8C%E6%8E%92%E7%89%88%E3%80%8D%E8%A6%8F%E5%89%87"},ws=o('ESLint 在 2023 年 10 月宣布將棄用排版(Formatting)規則 ,也就是棄用那些跟空格、縮排、換行、單/雙引號、分號等 相關規則。而其餘分類下的規則不受影響,例如強制使用嚴格等於 ===
(eqeqeq )、強制命名小駝峰(camelcase )等。
未來若想繼續透過 ESLint 而非 Prettier 來排版程式碼,可以考慮搭配 ESLint Stylistic 來達成一樣的效果。這個 plugin 將會繼續接棒,維護這些被棄用的規則。
',2),Ss={id:"%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99",tabindex:"-1"},Vs=n("參考資料 "),Os={class:"header-anchor",href:"#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"},Ls=s("p",null,[s("a",{href:"https://github.com/vitejs/vite/issues/13739#issuecomment-1641380518"},"https://github.com/vitejs/vite/issues/13739#issuecomment-1641380518"),s("br"),s("a",{href:"https://juejin.cn/post/7126043888573218823"},"https://juejin.cn/post/7126043888573218823")],-1),Ys={title:"建立 Vite + Vue + TypeScript + ESLint 專案可能會遇到的坑",date:"2023/10/03",tags:["Vite","Vue","TypeScript","ESLint"],original:"https://hackmd.io/@xq/vite-vue-ts-eslint-setup"},Rs="",Hs=p({__name:"vite-vue-ts-eslint-setup",setup(Ts,{expose:l}){return l({frontmatter:{title:"建立 Vite + Vue + TypeScript + ESLint 專案可能會遇到的坑",date:"2023/10/03",tags:["Vite","Vue","TypeScript","ESLint"],original:"https://hackmd.io/@xq/vite-vue-ts-eslint-setup"},excerpt:void 0}),(Ps,Ws)=>{const a=t("icon-link");return c(),i("div",D,[q,s("h2",_,[k,s("a",v,[e(a)])]),C,s("h2",m,[F,s("a",b,[e(a)])]),j,s("h2",x,[w,s("a",S,[e(a)])]),V,s("h2",O,[L,s("a",T,[e(a)])]),P,s("h2",W,[N,s("a",Y,[e(a)])]),R,s("h2",H,[I,Q,G,s("a",J,[e(a)])]),z,s("h2",K,[M,s("a",U,[e(a)])]),s("h3",X,[Z,$,s("a",ss,[e(a)])]),ns,s("h3",as,[es,os,s("a",ls,[e(a)])]),ps,s("h2",ts,[cs,s("a",is,[e(a)])]),rs,s("h2",ds,[us,s("a",ys,[e(a)])]),As,s("h2",hs,[Es,s("a",gs,[e(a)])]),fs,Bs,s("h3",Ds,[qs,s("a",_s,[e(a)])]),ks,s("h3",vs,[Cs,s("a",ms,[e(a)])]),Fs,s("h2",bs,[js,s("a",xs,[e(a)])]),ws,s("h2",Ss,[Vs,s("a",Os,[e(a)])]),Ls])}}});export{Hs as default,Rs as excerpt,Ys as frontmatter};
diff --git a/assets/vscode-auto-fix-on-save-445e3f9d.gif b/assets/vscode-auto-fix-on-save-445e3f9d.gif
new file mode 100644
index 00000000..5ce38df8
Binary files /dev/null and b/assets/vscode-auto-fix-on-save-445e3f9d.gif differ
diff --git a/assets/vscode-linting-result-f315bd6a.png b/assets/vscode-linting-result-f315bd6a.png
new file mode 100644
index 00000000..7445524f
Binary files /dev/null and b/assets/vscode-linting-result-f315bd6a.png differ
diff --git a/assets/vue-file-error-73be3796.png b/assets/vue-file-error-73be3796.png
new file mode 100644
index 00000000..69d67f6d
Binary files /dev/null and b/assets/vue-file-error-73be3796.png differ
diff --git a/assets/webpack-config-esm-320be40c.js b/assets/webpack-config-esm-320be40c.js
new file mode 100644
index 00000000..6ba505f2
--- /dev/null
+++ b/assets/webpack-config-esm-320be40c.js
@@ -0,0 +1,19 @@
+import{d as c,v as t,o as p,a as r,h as s,j as n,k as e,A as o}from"./app-033a95d9.js";const i={class:"markdown-body"},d={id:"%E6%AD%A5%E9%A9%9F",tabindex:"-1"},h=e("步驟 "),b={class:"header-anchor",href:"#%E6%AD%A5%E9%A9%9F"},_={id:"%E5%AE%89%E8%A3%9D-%40babel%2Fregister-%E8%88%87-babel-preset-env",tabindex:"-1"},k=e("安裝 "),A=s("code",{class:""},"@babel/register",-1),E=e(" 與 "),f=s("code",{class:""},"babel-preset-env",-1),y=e(),g={class:"header-anchor",href:"#%E5%AE%89%E8%A3%9D-%40babel%2Fregister-%E8%88%87-babel-preset-env"},u=o(`npm install -D @babel/core @babel/register babel-preset-env
+
npm install -D @babel/core @babel/register babel-preset-env
+
`,1),B={id:"%E8%A8%AD%E5%AE%9A-.babelrc",tabindex:"-1"},D=e("設定 "),m=s("code",{class:""},".babelrc",-1),v=e(),w={class:"header-anchor",href:"#%E8%A8%AD%E5%AE%9A-.babelrc"},q=o(`{
+ "presets": [
++ "@babel/preset-env"
+ ],
+}
+
+
{
+ "presets": [
++ "@babel/preset-env"
+ ],
+}
+
+
`,1),x={id:"%E6%9B%B4%E6%96%B0-webpack-%E8%A8%AD%E5%AE%9A",tabindex:"-1"},C=e("更新 webpack 設定 "),F={class:"header-anchor",href:"#%E6%9B%B4%E6%96%B0-webpack-%E8%A8%AD%E5%AE%9A"},j=o(`將原 webpack.config.js
更名為 webpack.config.babel.js
在專案根目錄建立 webpack.config.js
require ( '@babel/register' )
+module . exports = require ( './webpack.config.babel.js' )
+
require ( '@babel/register' )
+module . exports = require ( './webpack.config.babel.js' )
+
`,1),N={id:"%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99",tabindex:"-1"},V=e("參考資料 "),S={class:"header-anchor",href:"#%E5%8F%83%E8%80%83%E8%B3%87%E6%96%99"},M=s("ul",null,[s("li",null,[s("a",{href:"https://stackoverflow.com/a/43165210/12970551"},"https://stackoverflow.com/a/43165210/12970551")]),s("li",null,[s("a",{href:"https://stackoverflow.com/a/52092788/12970551"},"https://stackoverflow.com/a/52092788/12970551")])],-1),H={title:"在 webpack.config.js 裡使用 ESM 的 import 語法",date:"2022/02/20",tags:["Webpack"],original:"https://hackmd.io/@xq/webpack-config-esm"},I="",J=c({__name:"webpack-config-esm",setup(W,{expose:l}){return l({frontmatter:{title:"在 webpack.config.js 裡使用 ESM 的 import 語法",date:"2022/02/20",tags:["Webpack"],original:"https://hackmd.io/@xq/webpack-config-esm"},excerpt:void 0}),(T,z)=>{const a=t("icon-link");return p(),r("div",i,[s("h2",d,[h,s("a",b,[n(a)])]),s("h3",_,[k,A,E,f,y,s("a",g,[n(a)])]),u,s("h3",B,[D,m,v,s("a",w,[n(a)])]),q,s("h3",x,[C,s("a",F,[n(a)])]),j,s("h2",N,[V,s("a",S,[n(a)])]),M])}}});export{J as default,I as excerpt,H as frontmatter};
diff --git a/assets/wikipedia-link-converter-5743973b.js b/assets/wikipedia-link-converter-5743973b.js
new file mode 100644
index 00000000..474a740b
--- /dev/null
+++ b/assets/wikipedia-link-converter-5743973b.js
@@ -0,0 +1 @@
+import{d as n,v as r,o as c,a as _,h as e,j as s,k as t}from"./app-033a95d9.js";const i="/assets/before-7a436972.png",d="/assets/after-97d65a70.png",h={class:"markdown-body"},l={id:"%E8%85%B3%E6%9C%AC%E9%80%A3%E7%B5%90",tabindex:"-1"},p=t("腳本連結 "),m={class:"header-anchor",href:"#%E8%85%B3%E6%9C%AC%E9%80%A3%E7%B5%90"},E=e("p",null,[e("a",{href:"https://gist.github.com/ngseke/5d0f9ef02aa320e969768aaa8e5d888a"},"https://gist.github.com/ngseke/5d0f9ef02aa320e969768aaa8e5d888a")],-1),f={id:"%E8%AA%AA%E6%98%8E",tabindex:"-1"},g=t("說明 "),k={class:"header-anchor",href:"#%E8%AA%AA%E6%98%8E"},B=e("p",null,"不知道從何時開始,Google 搜尋結果的維基百科連結,在不知不覺間都變成了行動版本。然而行動版在筆電上總有一股違和感,也可能是我早已看習慣舊版的了。",-1),u=e("p",null,[t("於是我寫了一段簡單的 "),e("a",{href:"https://www.tampermonkey.net/"},"Tampermonkey"),t(" 腳本,用來轉換 Google 搜尋結果中所有的維基百科連結,將行動版轉換成桌面版,也就是把網址的 "),e("code",{class:""},".m"),t(" 部分給去掉。")],-1),A={id:"%E8%BD%89%E6%8F%9B%E7%B5%90%E6%9E%9C",tabindex:"-1"},b=t("轉換結果 "),x={class:"header-anchor",href:"#%E8%BD%89%E6%8F%9B%E7%B5%90%E6%9E%9C"},C=e("p",null,[e("strong",null,"Before:"),e("br"),e("img",{src:i,alt:"before"})],-1),v=e("p",null,[e("strong",null,"After:"),e("br"),e("img",{src:d,alt:"after"})],-1),N={title:"還給你 Google 搜尋結果的桌面版維基百科",date:"2022/05/27",tags:["Tampermonkey"]},V="",D=n({__name:"wikipedia-link-converter",setup(w,{expose:a}){return a({frontmatter:{title:"還給你 Google 搜尋結果的桌面版維基百科",date:"2022/05/27",tags:["Tampermonkey"]},excerpt:void 0}),(y,G)=>{const o=r("icon-link");return c(),_("div",h,[e("h2",l,[p,e("a",m,[s(o)])]),E,e("h2",f,[g,e("a",k,[s(o)])]),B,u,e("h2",A,[b,e("a",x,[s(o)])]),C,v])}}});export{D as default,V as excerpt,N as frontmatter};
diff --git a/assets/youtube-cc-menu-f1682830.png b/assets/youtube-cc-menu-f1682830.png
new file mode 100644
index 00000000..23880fd2
Binary files /dev/null and b/assets/youtube-cc-menu-f1682830.png differ
diff --git a/blog.html b/blog.html
new file mode 100644
index 00000000..ccad0465
--- /dev/null
+++ b/blog.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Blog | ngseke
+
+
+
+
+
+
\ No newline at end of file
diff --git a/blog/bootstrap-in-nuxt.html b/blog/bootstrap-in-nuxt.html
new file mode 100644
index 00000000..643810d1
--- /dev/null
+++ b/blog/bootstrap-in-nuxt.html
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 在 Nuxt 專案穩穩地匯入 Bootstrap | ngseke
+
+ 在 Nuxt 專案穩穩地匯入 Bootstrap Nov 21, 2021 by Sean Huang 此筆記指的是純 bootstrap ,與 bootstrap-vue 無關。
下文出現的 BS 是 Bootstrap 的簡寫
🌟 這篇筆記的重點有哪些? 與直接匯入編譯過並打包好的 css 相比,此作法:
保留自定義樣式的能力,優雅地從根源自定義主題,免去用層層的自訂 class 複寫 避免組件充斥著 @media (min-width: 576px) { ... }
諸如此類的魔法數字 節省專案建構大小 🔧 安裝 Bootstrap npm i bootstrap
+
npm i bootstrap
+
🔧 安裝 @nuxtjs/style-resources
npm i @nuxtjs/style-resources
+
npm i @nuxtjs/style-resources
+
📃 建立 _bootstrap.sass
建立 assets/sass/style-resources/_bootstrap.sass
(檔名隨意)
在裡面匯入 BS 的源 SCSS 檔案,這三行是必須的。
// _bootstrap.sass
+
+@import "~bootstrap/scss/functions"
+@import "~bootstrap/scss/variables"
+@import "~bootstrap/scss/mixins"
+
// _bootstrap.sass
+
+@import "~bootstrap/scss/functions"
+@import "~bootstrap/scss/variables"
+@import "~bootstrap/scss/mixins"
+
https://getbootstrap.com/docs/4.6/getting-started/theming/#importing
📃 建立 _custom.sass
建立 assets/sass/style-resources/_custom.sass
(檔名隨意)。
此檔用來複寫 BS 預設變數。
// _custom.sass
+
+// 🌰 舉例:
+
+// 將「primary」換成黃色
+$theme-colors : ( primary : #ffd019 )
+
+// 將連結換成黃色,並移除 hover 的底線
+$link-color : #ffd019
+$link-hover-decoration : none
+
+// 重新定義斷點大小
+$grid-breakpoints : ( xs : 0 , sm : 576 px , md : 768 px , lg : 992 px , xl : 1200 px , xxl : 1400 px )
+
+/* ... */
+
// _custom.sass
+
+// 🌰 舉例:
+
+// 將「primary」換成黃色
+$theme-colors : ( primary : #ffd019 )
+
+// 將連結換成黃色,並移除 hover 的底線
+$link-color : #ffd019
+$link-hover-decoration : none
+
+// 重新定義斷點大小
+$grid-breakpoints : ( xs : 0 , sm : 576 px , md : 768 px , lg : 992 px , xl : 1200 px , xxl : 1400 px )
+
+/* ... */
+
更多可供複寫的變數都列在此處:https://github.com/twbs/bootstrap/blob/main/scss/_variables.scss
📃 建立 style.sass
建立 assets/sass/style.sass
(檔名隨意)
根據各自需求匯入 BS 元件,這些元件將會套用前面設定的主題變數。
// style.sass
+
+// 👍 推薦的元件
+@import "~bootstrap/scss/reboot"
+@import "~bootstrap/scss/utilities"
+@import "~bootstrap/scss/grid"
+@import "~bootstrap/scss/type"
+
+// 其他常用元件
+@import "~bootstrap/scss/images"
+@import "~bootstrap/scss/nav"
+@import "~bootstrap/scss/navbar"
+
+/* ... */
+
// style.sass
+
+// 👍 推薦的元件
+@import "~bootstrap/scss/reboot"
+@import "~bootstrap/scss/utilities"
+@import "~bootstrap/scss/grid"
+@import "~bootstrap/scss/type"
+
+// 其他常用元件
+@import "~bootstrap/scss/images"
+@import "~bootstrap/scss/nav"
+@import "~bootstrap/scss/navbar"
+
+/* ... */
+
可用的元件列表及用途請參考此處:https://github.com/twbs/bootstrap/tree/main/scss
✏️ 配置 nuxt.config.js
export default {
+ /* ... */
+
+ styleResources : {
+ sass : [
+ './assets/sass/style-resources/_custom.sass' ,
+ './assets/sass/style-resources/_bootstrap.sass' ,
+ ],
+ },
+
+ css : [
+ './assets/sass/style.sass' ,
+ ],
+}
+
export default {
+ /* ... */
+
+ styleResources : {
+ sass : [
+ './assets/sass/style-resources/_custom.sass' ,
+ './assets/sass/style-resources/_bootstrap.sass' ,
+ ],
+ },
+
+ css : [
+ './assets/sass/style.sass' ,
+ ],
+}
+
⚠️ 注意 是在 css
匯入 style.sass
,而不是在styleResources
匯入。 在 styleResources
匯入的話,將使得每個 Vue 組件 都包含一大包的 Bootstrap 樣式,造成專案容量爆肥,編譯速度也會被嚴重拖慢。
🖖 在 Vue 組件內使用 🎉 大功告成!
如此便可自由自在地在 vue 組件內使用 BS 的 class 和所有 SCSS/SASS 變數。
< template > /* ... */ </ template >
+< script > /* ... */ </ script >
+
+< style lang = "sass" scoped >
+div
+ @include media-breakpoint-down(sm)
+ background: $primary
+</ style >
+
< template > /* ... */ </ template >
+< script > /* ... */ </ script >
+
+< style lang = "sass" scoped >
+div
+ @include media-breakpoint-down(sm)
+ background: $primary
+</ style >
+
+
+
+
+
\ No newline at end of file
diff --git a/blog/check-if-key-exists.html b/blog/check-if-key-exists.html
new file mode 100644
index 00000000..4bacd41b
--- /dev/null
+++ b/blog/check-if-key-exists.html
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 檢查物件的 key 是否存在的 N 種方法 | ngseke
+
+ 檢查物件的 key 是否存在的 N 種方法 Jul 07, 2023 by Sean Huang .hasOwnProperty()
使用物件的 prototype hasOwnProperty
檢查。
不會 遍歷原型鏈。
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+}
+
+profile . hasOwnProperty ( 'name' ) // true
+profile . hasOwnProperty ( 'valueOf' ) // false
+
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+}
+
+profile . hasOwnProperty ( 'name' ) // true
+profile . hasOwnProperty ( 'valueOf' ) // false
+
in
最簡潔的寫法,使用關鍵字 in
檢查。
但請留意 in
會 去尋找整個原型鏈,可能得到預期外的結果,並造成微乎其微的效能差異。
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+}
+
+'name' in profile // true
+
+/*
+ ⚠️ 雖然以下 key 並沒有在 `profile` 中明確定義,
+ 但因為它們存在於物件的 prototype 中,所以依然會得到 true。
+*/
+'valueOf' in profile // true
+'toString' in profile // true
+'hasOwnProperty' in profile // true
+
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+}
+
+'name' in profile // true
+
+/*
+ ⚠️ 雖然以下 key 並沒有在 `profile` 中明確定義,
+ 但因為它們存在於物件的 prototype 中,所以依然會得到 true。
+*/
+'valueOf' in profile // true
+'toString' in profile // true
+'hasOwnProperty' in profile // true
+
效能實測:hasOwnProperty 和 in 哪个性能高? - frosen的回答 - 知乎
Object.prototype.hasOwnProperty.call()
由於 JavaScript 未保護 hasOwnProperty
,所以你完全可以複寫這個屬性,讓它刻意回傳錯誤的結果。
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+ hasOwnProperty : () => true ,
+}
+
+/* 😢 現在無論傳入什麼都會得到 true */
+profile . hasOwnProperty ( '🍺' ) // true
+profile . hasOwnProperty ( 123 ) // true
+
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+ hasOwnProperty : () => true ,
+}
+
+/* 😢 現在無論傳入什麼都會得到 true */
+profile . hasOwnProperty ( '🍺' ) // true
+profile . hasOwnProperty ( 123 ) // true
+
利用 Object.prototype.hasOwnProperty.call()
即可避免此情況發生,也不會 遍歷原型鏈。
/* 😎 雖然冗長但最安全 */
+Object . prototype . hasOwnProperty . call ( profile , 'name' ) // true
+Object . prototype . hasOwnProperty . call ( profile , '🍺' ) // false
+
/* 😎 雖然冗長但最安全 */
+Object . prototype . hasOwnProperty . call ( profile , 'name' ) // true
+Object . prototype . hasOwnProperty . call ( profile , '🍺' ) // false
+
Object.hasOwn()
ES13 推出的新特性,旨在取代 Object.prototype.hasOwnProperty()
,寫相比之下法更直觀和簡潔。並且和 Object.prototype.hasOwnProperty.call()
一樣,即使複寫了 hasOwnProperty
依然可以得到正確的結果。
不會 遍歷原型鏈。
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+ hasOwnProperty : () => true ,
+}
+
+Object . hasOwn ( profile , 'name' ) // true
+Object . hasOwn ( profile , '🍺' ) // false
+
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+ hasOwnProperty : () => true ,
+}
+
+Object . hasOwn ( profile , 'name' ) // true
+Object . hasOwn ( profile , '🍺' ) // false
+
其他常見但有陷阱的方法 Object.keys().includes()
😐 先以 Object.keys()
取得物件的所有 key 的陣列,接著呼叫陣列的方法 includes()
來檢查 key 是否存在。
但這個方法的時間複雜度是 O(n)
(或更高 ),因為它必須先至少遍歷物件來得到所有的 key,接著尋找時又得再遍歷一次陣列,在 key 數量一多時顯然很沒效率。
不會 遍歷原型鏈。
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+}
+
+const keys = Object . keys ( profile ) // ['name', 'age']
+
+keys . includes ( 'name' ) // true
+keys . includes ( 'valueOf' ) // false
+
const profile = {
+ name : 'Sean' ,
+ age : 24 ,
+}
+
+const keys = Object . keys ( profile ) // ['name', 'age']
+
+keys . includes ( 'name' ) // true
+keys . includes ( 'valueOf' ) // false
+
!== undefined
😐 當試圖存取不存在於物件的 key 時,會得到 undefined
。
但當某 key 存在而且值剛好是 undefined
時,那就仍會得到 false
。
會 遍歷原型鏈。
const profile = {
+ name : 'Sean' ,
+ phone : undefined ,
+}
+
+profile . address !== undefined // false
+profile . phone !== undefined // ⚠️ false
+
const profile = {
+ name : 'Sean' ,
+ phone : undefined ,
+}
+
+profile . address !== undefined // false
+profile . phone !== undefined // ⚠️ false
+
!!
或 Boolean()
😕 簡單暴力的寫法,也就是直接將值轉型成 boolean。但這方法顯然很不可靠,因為只要是 falsy 值,例如 0
、空字串 ''
、 null
等 ,即使 key 存在但依然會得到 false
。
除非你對物件型別有十足的信心,例如在有 TypeScript 的場合,否則不太推薦這寫法。
會 遍歷原型鏈。
const profile = {
+ name : 'Sean' ,
+ balance : 0 ,
+ isDead : false ,
+}
+
+!! profile . name // true
+!! profile . balance // ⚠️ false
+Boolean ( profile . isDead ) // ⚠️ false
+
const profile = {
+ name : 'Sean' ,
+ balance : 0 ,
+ isDead : false ,
+}
+
+!! profile . name // true
+!! profile . balance // ⚠️ false
+Boolean ( profile . isDead ) // ⚠️ false
+
參考資料
+
+
+
+
\ No newline at end of file
diff --git a/blog/code-review-taxi-number.html b/blog/code-review-taxi-number.html
new file mode 100644
index 00000000..381422c7
--- /dev/null
+++ b/blog/code-review-taxi-number.html
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 如果太認真去 Code Review 會發生什麼事? | ngseke
+
+ 如果太認真去 Code Review 會發生什麼事? May 31, 2023 by Sean Huang 前言 Code Review 實在是太重要了,我想沒人會否認。
在軟體開發的世界中,需求不斷地變化是家常便飯,程式碼的可讀性和可維護性顯得格外重要。然而 Code Review 要看的多深、多仔細、花多少時間看,想找到適當的平衡點,其中分寸的拿捏確實是個挑戰。
但我始終認為有總比沒有好,畢竟 Code Review 是維持程式碼品質最有效的方式。相信大部分開發者都有同感: 「不是新需求難實現,而是舊的扣改不動。」 有多少人曾被困在遺留代碼之中,為了加一個新功能,首先得耗費無數的精力,試圖理解那些又長又冗、變數胡亂命名的程式碼。接著才發現函式和函式之間耦合的嚴重,想要重構又因為副作用太多而不敢輕舉妄動。
因此在這篇文章中我想模擬這樣一個情境:
如果很嚴格地進行 Code Review,最吹毛求疵的那種,那看起來會像什麼樣子?
我將以各種壞味道的角度,分析一段出現在真實場景的程式碼,並提供修改建議。同時也分享可套用的對應 ESLint 規則,其官方文件也會解釋這條規則存在的理由是什麼。
Review 原則 程式碼應具可讀性 以盡量少的程式碼實現相同的功能 避免潛在副作用和壞味道 需求描述 函式接受任意字串,轉換成用來表示「計程車的數量」的數字字串,介於 0
至 30
小於 0
時應總是得到 0
;大於 30
時總是應得到 30
無法用 parseInt
解析的字串一律轉換成 0
轉換後的字串不足兩位數時,左邊補上 0
原程式碼片段 這 13 行代碼包括註解一字不差地擷自真實專案,其中的人名已替換成假名
// 2021-11-23 Sean : 轉型 > 補0
+const TaxiNumber = ( v ) => {
+ // 正規表示
+ let regex = new RegExp ( / ^ \d {1,3} $ / );
+ // 叫車最小值、最大值
+ let minTaxi = 0 , maxTaxi = 30 ;
+ // 轉型,利用轉型與輸入特性,處裡數字後字串
+ v = parseInt ( v );
+ // 字串判斷?數值判斷
+ v = regex . test ( v . toString ()) ? v < minTaxi ? minTaxi : v > maxTaxi ? maxTaxi : v : minTaxi ;
+ // 回傳 & 補0
+ return v . toString (). padStart ( 2 , '0' );
+}
+
// 2021-11-23 Sean : 轉型 > 補0
+const TaxiNumber = ( v ) => {
+ // 正規表示
+ let regex = new RegExp ( / ^ \d {1,3} $ / );
+ // 叫車最小值、最大值
+ let minTaxi = 0 , maxTaxi = 30 ;
+ // 轉型,利用轉型與輸入特性,處裡數字後字串
+ v = parseInt ( v );
+ // 字串判斷?數值判斷
+ v = regex . test ( v . toString ()) ? v < minTaxi ? minTaxi : v > maxTaxi ? maxTaxi : v : minTaxi ;
+ // 回傳 & 補0
+ return v . toString (). padStart ( 2 , '0' );
+}
+
壞味道一:意圖不明的函式命名 const TaxiNumber = ( v ) => {
+ ^^^^^^^^^^
+
const TaxiNumber = ( v ) => {
+ ^^^^^^^^^^
+
我首先注意到 TaxiNumber
這個單詞是大駝峰,當讀者單獨看時很容易產生困惑:它是用來展示計程車數字的 React Component 嗎?又或是 class ?但都不是,它只是用來轉換字串的函式。
函式通常是以動詞開頭,而且應命名得清晰明瞭,能夠表達其功能或操作目的。
這類函式建議以 convert
、parse
、format
、normalize
等動詞開頭,例如 convertAToB
,且按照 JavaScript 慣例通常會以小駝峰命名。
💡 套用 ESLint 規則 camelcase
可約束一致變數的命名風格。
壞味道二:縮寫的參數 const TaxiNumber = ( v ) => {
+ ^
+
const TaxiNumber = ( v ) => {
+ ^
+
v
可能是 value
的縮寫,隨個人喜好略縮單詞是個常見的壞味道,通常只有作者本人才知道其背後的含義。但要知道身為團隊的一員,自己不會是唯一維護這段程式碼的人。命名前多花個幾秒仔細思考,將心比心地為下一個接手的人,甚至是一個月後的自己著想,確保它清晰易懂,任何人看了都能夠馬上理解。
建議替換成 countString
,或是任意一具描述性單詞。
💡 套用 ESLint 規則 id-denylist
可以定義「禁詞」,禁止使用這些單詞來命名,例如單字母 a
b
c
,或是毫無意義的 data
、value
。
壞味道三:冗余的建構式 let regex = new RegExp ( / ^ \d {1,3} $ / );
+ ^^^^^^^^^^
+
let regex = new RegExp ( / ^ \d {1,3} $ / );
+ ^^^^^^^^^^
+
在這個情境並沒打算以 template literals 來動態產生 pattern,例如 new RegExp(`^{${a}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
,所以可直接省略掉 new RegExp()
,直接寫成更簡短的 /^\d{1,3}$/
即可。
💡 套用 ESLint 規則 prefer-regex-literals
來偏好採用 regular expression literals 寫法。
當能用更簡潔的寫法達成一模一樣的效果時,就沒理由選擇冗長的那個。其他類似的冗余的建構式還有這些:
寫 {}
,而非 new Object()
寫 []
,而非 new Array()
💡 套用 ESLint 規則 no-new-object
和 no-array-constructor
來偏好採用 literal notation 寫法。
壞味道四:從未改變的 let
let minTaxi = 0 , maxTaxi = 30 ;
+ ^^^
+
let minTaxi = 0 , maxTaxi = 30 ;
+ ^^^
+
一個簡單的原則是永遠優先使用 const
來宣告變數,除非真的有需要重新賦值。因此建議改寫成:
/* 常見風格 */
+const min = 0 , max = 30
+
+/* 陣列解構風格 */
+const [ min , max ] = [ 0 , 30 ]
+
/* 常見風格 */
+const min = 0 , max = 30
+
+/* 陣列解構風格 */
+const [ min , max ] = [ 0 , 30 ]
+
順帶一提, [number, number]
的寫法在 TypeScript 中稱作 tuple ,在這隱含一種成雙成對的概念,有助於讀者把它們理解成是「一組範圍」的數字。
💡 套用 ESLint 規則 prefer-const
檢查從未被重新賦值的變數。支援 auto fix,可以自動將 let
替換成 const
。
壞味道五:對參數重新賦值 v = parseInt ( v );
+ ^
+
v = parseInt ( v );
+ ^
+
無論任何語言,複寫函式的參數 從來不是個好主意,可參考一些針對 reassign parameter 的討論 。因為這很容易誤導讀者,原本預期傳入的參數是 string,但突然在某一行後卻硬生生變成了 number。
若專案有導入 TypeScript,某種程度也可以避免出現這種寫法,例如在此例中,因為新值顯然不符合參數原本的型別,compiler 便會直接報錯。
💡 套用 ESLint 規則 no-param-reassign
來防止參數被 reassign。
壞味道六:冗長的三元運算子 v = regex . test ( v . toString ()) ? v < minTaxi ? minTaxi : v > maxTaxi ? maxTaxi : v : minTaxi ;
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
v = regex . test ( v . toString ()) ? v < minTaxi ? minTaxi : v > maxTaxi ? maxTaxi : v : minTaxi ;
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
巢狀的三元運算子,多個 :
與 ?
交錯,沒有換行與縮排,缺乏可讀性。
建議採用具有更明確的語意的 Math.min()
和 Math.max()
方法改寫,並加上適當的換行和縮排。
v = regex . test ( v . toString ())
+ ? Math . min ( Math . max ( v , minTaxi ), maxTaxi )
+ : minTaxi
+
v = regex . test ( v . toString ())
+ ? Math . min ( Math . max ( v , minTaxi ), maxTaxi )
+ : minTaxi
+
💡 套用 ESLint 規則 no-nested-ternary
來禁止巢狀的三元運算子。
壞味道七:深入變數的命名藝術 TaxiNumber
number
一詞作為變數有點太籠統了,也沒有好好地起到描述的作用,說到底任何只要是數值想必都會是 number。例如寬度、角度和價格它們都是 number。
要描述這類變數,相信一定找得到比 number
還更貼切的單詞。
根據需求它是用來表示「計程車的數量」,對於這類計數/累加 的變數,建議可命名成 count
。
minTaxi
第一眼見到 minTaxi
和 maxTaxi
,有點難想像其背後的含義是什麼。讀者可能會把他誤解成車型的大小,或是計程車的載客數。
在這個情況建議單純命名成 min
就行,把 taxi
當作後綴反而不適合,因為實際上要描述的是計程車的「數量」而非「計程車本身」。若想更精準的描述,可以考慮用 minCount
或 minTaxiCount
。
壞味道八:「// 到此一遊
」 // 2021-11-23 Sean : 轉型 > 補0
+
// 2021-11-23 Sean : 轉型 > 補0
+
在未導入版控的遺留專案中,常見在註解裡出現作者的大名。但在現代軟體開發早以 git 進行版控,透過 git blame 就能追溯每一行的修改者是誰、何時修改的、以及對應的 commit message 等。
// 正規表示
+ // 叫車最小值、最大值
+ // 轉型,利用轉型與輸入特性,處裡數字後字串
+ // 字串判斷?數值判斷
+ // 回傳 & 補0
+
// 正規表示
+ // 叫車最小值、最大值
+ // 轉型,利用轉型與輸入特性,處裡數字後字串
+ // 字串判斷?數值判斷
+ // 回傳 & 補0
+
事實上,不只第一行的註解,這整段所有的註解可能都是多餘的,因為程式碼做的事太顯而易見了,註解反而變成閱讀時的雜訊。善用能夠自我描述的變數和函式命名是最好的方法,因為它們本身就能夠清楚地傳達代碼的意圖,不再需要額外的註解。
最終重構結果 經過分析不難發現這個函式沒必要用上 RegExp,單純用 parseInt()
就足夠了。因為當傳入不合法的字串時,它就會一律回傳 NaN
,也不會拋出任何錯誤。KISS(Keep It Simple, Stupid)原則 便很好地說明了這個道理,複雜的設計往往容易出錯,增加不必要的複雜度和 debug 的難度,因此應傾向以簡單和直接的方式解決問題,以保持程式碼的清晰度和可讀性。
綜合上述幾點,經過 TypeScript 重構後的程式碼可以長得像這樣:
const parseTaxiCount = ( countString : string ) => {
+ const [ min , max ] = [ 0 , 30 ]
+ const count = parseInt ( countString ) || 0
+ const clampedCount = Math . min ( Math . max ( count , min ), max )
+ const paddedCount = String ( clampedCount ). padStart ( 2 , '0' )
+
+ return paddedCount
+}
+
const parseTaxiCount = ( countString : string ) => {
+ const [ min , max ] = [ 0 , 30 ]
+ const count = parseInt ( countString ) || 0
+ const clampedCount = Math . min ( Math . max ( count , min ), max )
+ const paddedCount = String ( clampedCount ). padStart ( 2 , '0' )
+
+ return paddedCount
+}
+
最後也附上單元測試(Jest):
it ( 'parseTaxiCount()' , () => {
+ expect ( parseTaxiCount ( '' )). toBe ( '00' )
+ expect ( parseTaxiCount ( ' ' )). toBe ( '00' )
+ expect ( parseTaxiCount ( '-1' )). toBe ( '00' )
+ expect ( parseTaxiCount ( '0' )). toBe ( '00' )
+ expect ( parseTaxiCount ( '1' )). toBe ( '01' )
+ expect ( parseTaxiCount ( '01' )). toBe ( '01' )
+ expect ( parseTaxiCount ( '15' )). toBe ( '15' )
+ expect ( parseTaxiCount ( '30' )). toBe ( '30' )
+ expect ( parseTaxiCount ( '31' )). toBe ( '30' )
+ expect ( parseTaxiCount ( '999' )). toBe ( '30' )
+ expect ( parseTaxiCount ( 'abc' )). toBe ( '00' )
+ expect ( parseTaxiCount ( '1a1' )). toBe ( '01' )
+ expect ( parseTaxiCount ( '30aaa' )). toBe ( '30' )
+ expect ( parseTaxiCount ( '030aaa' )). toBe ( '30' )
+})
+
it ( 'parseTaxiCount()' , () => {
+ expect ( parseTaxiCount ( '' )). toBe ( '00' )
+ expect ( parseTaxiCount ( ' ' )). toBe ( '00' )
+ expect ( parseTaxiCount ( '-1' )). toBe ( '00' )
+ expect ( parseTaxiCount ( '0' )). toBe ( '00' )
+ expect ( parseTaxiCount ( '1' )). toBe ( '01' )
+ expect ( parseTaxiCount ( '01' )). toBe ( '01' )
+ expect ( parseTaxiCount ( '15' )). toBe ( '15' )
+ expect ( parseTaxiCount ( '30' )). toBe ( '30' )
+ expect ( parseTaxiCount ( '31' )). toBe ( '30' )
+ expect ( parseTaxiCount ( '999' )). toBe ( '30' )
+ expect ( parseTaxiCount ( 'abc' )). toBe ( '00' )
+ expect ( parseTaxiCount ( '1a1' )). toBe ( '01' )
+ expect ( parseTaxiCount ( '30aaa' )). toBe ( '30' )
+ expect ( parseTaxiCount ( '030aaa' )). toBe ( '30' )
+})
+
結語 以上分析基於個人開發習慣,提供一些在 Code Review 時可作為參考的觀點,其中很多屬於很主觀的意見,未必是最佳解。因此每個團隊都應該對程式碼風格有所共識,你會發現其實絕大部分都可以交給 ESLint 等自動化工具檢查,將寶貴的 Code Review 時間花在確認是否符合需求、架構的設計是否合理、評估演算法的效能和擴展性等。
Code Review 的目的不僅僅是確保程式碼的品質和一致性,更重要的是促進團隊之間的合作和溝通。這是一個讓開發者互相學習、分享最佳實踐和技巧,以及達成共識的過程。因此在這個過程中,要盡可能保持開放和建設性的討論,避免淪為單純的批評和指責。
+
+
+
+
\ No newline at end of file
diff --git a/blog/component-naming.html b/blog/component-naming.html
new file mode 100644
index 00000000..f631541e
--- /dev/null
+++ b/blog/component-naming.html
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 試著用有點違反直覺的方式命名組件 | ngseke
+
+ 試著用有點違反直覺的方式命名組件 Nov 28, 2021 by Sean Huang 前言:30 秒越南語小教室 🇻🇳 越南語是形容詞 後置的語言,恰恰和華語或英語相反。
例如「越南語」這個詞會寫成 「語越南」 :
Tiếng Việt Nam
+ ( 語 ) ( 越南 )
+
Tiếng Việt Nam
+ ( 語 ) ( 越南 )
+
「台灣人」則會寫做 「人台灣」 :
Người Đài Loan
+ ( 人 ) ( 台灣 )
+
Người Đài Loan
+ ( 人 ) ( 台灣 )
+
修飾語擺後面 在 Vue 官方風格指南中,組件命名的概念與越南語文法不謀而合。
若按照英語的文法,人們通常習慣這樣命名組件:
Primary (a.) + Button (n.) = Primary Button Date (n.) + TextField (n.) = Date TextField Confirm (v.) + Dialog (n.) = Confirm Dialog 但如指南所述,它認為應該把🔵主語/主詞 (文中所謂高級別的詞 )擺到前面;🔴修飾性的詞 則是放在結尾。形容詞、動詞或名詞都可算是修飾詞。
因此推薦的命名法如下:
Button (n.) + Primary (a.) = Button Primary TextField (n.) + Date (n.) = TextField Date Dialog (n.) + Confirm (v.) = Dialog Confirm 在特定語境內的組件 對於在🟢特定語境 內才有意義的的組件──也就是在某些個別頁面,或是只有在特定組件中用得到的子組件,指南中也建議以父組件或頁面名當作前綴
例如:
Search (父組件) + TextField + Date = Search TextField Date Search 組件內專用的日期輸入框 Search (父組件) + Button + Submit = Search Button Submit Search 組件內專用的送出按鈕 LandingPage (頁面) + List + News = LandingPage List News 只在 Landing Page 用到的新聞列表 LandingPage (頁面) + List + Features = LandingPage List Features 只在 Landing Page 用到的產品特色列表停止巢狀資料夾! 你可能試過以一層又一層 的目錄來區分組件,像是:
components /
+ |- LandingPage /
+ |- News /
+ |- Item . tsx
+ |- List . tsx
+ |- Title . tsx
+ |- Features . tsx
+ |- Item . tsx
+ |- List . tsx
+ |- Title . tsx
+
components /
+ |- LandingPage /
+ |- News /
+ |- Item . tsx
+ |- List . tsx
+ |- Title . tsx
+ |- Features . tsx
+ |- Item . tsx
+ |- List . tsx
+ |- Title . tsx
+
但你其實可以放心、大膽地將它們都放在同一層 資料夾。
重新調整後的目錄結構如下:
components /
+ |- LandingPageNewsItem . tsx
+ |- LandingPageNewsList . tsx
+ |- LandingPageNewsTitle . tsx
+ |- LandingPageFeaturesItem . tsx
+ |- LandingPageFeaturesList . tsx
+ |- LandingPageFeaturesTitle . tsx
+
components /
+ |- LandingPageNewsItem . tsx
+ |- LandingPageNewsList . tsx
+ |- LandingPageNewsTitle . tsx
+ |- LandingPageFeaturesItem . tsx
+ |- LandingPageFeaturesList . tsx
+ |- LandingPageFeaturesTitle . tsx
+
But Why? 檔案總管的排序 絕大多數 IDE 例如 VSCode,它們的檔案總管都是按照英文字母 排序的,所以前綴相同的那些文件,都會集中在一塊兒,從命名一眼就能看出哪些組件具有較高關聯性。
「滾動」比「展開資料夾」更無負擔 若要從茫茫組件庫的大海中找尋某個組件,一個一個點擊並展開深層的巢狀資料夾是非常惱人的。更別提有些打開、有些關閉的資料夾相互交錯混雜的畫面,看了更是令人煩躁不安。
相比之下,用滑鼠滾動在同一層目錄瀏覽,顯然更有效率且輕鬆許多。
移動組件的困難度 對於尚未導入 TypeScript 的專案,想移動組件可能是件大工程。因為 VSCode 可能無法理解組件之間的相依關係,而無法順利地自動進行重構。
假設有一個組件 components/searchBar/Input/DateIuput.tsx
,今天我想將它提升至上層的 components/Input/
目錄,讓其他更多組件共用。然而我別無選擇,只能在專案中進行的全域搜尋並取代路徑字串,同時也得背負意外替換到其他無辜字串的風險。
撞名組件在視覺上難以定位 利用 ⌘ + P
搜尋眾多同名的組件時,在 VSCode 裡很難快速定位到目標,因為它們乍看之下都一模一樣。
同樣地,出現在頁籤中的多個同名檔案也是讓人眼花撩亂。
結語 本文中所提到的命名方式並非強制性的,它只是一種風格,在實務上還是應該考量專案的規模,根據團隊固有的習慣來權衡。不過如果今天你要做的是個人的 side project(例如本專案 ),那我會非常推薦你不如大膽地嘗試看看。
參考資料
+
+
+
+
\ No newline at end of file
diff --git a/blog/customize-youtube-caption-font.html b/blog/customize-youtube-caption-font.html
new file mode 100644
index 00000000..9565ee9a
--- /dev/null
+++ b/blog/customize-youtube-caption-font.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 把 YouTube CC 字幕變成粉圓體 | ngseke
+
+ 把 YouTube CC 字幕變成粉圓體 Aug 14, 2022 by Sean Huang 腳本連結 https://gist.github.com/ngseke/8b30050e05703f35c753cd0ac6330028
說明 YouTube CC 字幕雖然有內建設定字體的功能,然而可用的選項並不多,這些選項也都是基於英文字體。
如果你像我一樣對圓體有莫名的愛好,那你一定不能錯過這個腳本。它可以將 YouTube 的字幕替換成任何你想要的字體,前提是你的電腦要先安裝好該字體。
const fontFamily = '"jf-openhuninn-1.1"'
+// ^^^^^^^^^^^^^^^^^
+
const fontFamily = '"jf-openhuninn-1.1"'
+// ^^^^^^^^^^^^^^^^^
+
將第 :13
行替換成想要的字體名稱即可,例如本例用的是開源圓體「jf open 粉圓」 ,注意必須保留裡面的雙引號。
結果 Before:
After:
+
+
+
+
\ No newline at end of file
diff --git a/blog/react-handler-type.html b/blog/react-handler-type.html
new file mode 100644
index 00000000..6ed94686
--- /dev/null
+++ b/blog/react-handler-type.html
@@ -0,0 +1,220 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 處理在 React 抽出 event handler 時常碰到的 TypeScript 參數型別問題 | ngseke
+
+ 處理在 React 抽出 event handler 時常碰到的 TypeScript 參數型別問題 Jun 06, 2022 by Sean Huang TL;DR 如果某個 event handler 的事件參數,就只是單純拿來呼叫 preventDefault()
或 stopPropagation()
,那麼無論它是什麼元素,只要將 event
指定成 SyntheticEvent
型別即可,寫起來也非常簡潔。
import { SyntheticEvent } from 'react' // 記得 import
+
+// ...
+
+const handleSubmit = ( event : SyntheticEvent ) => {
+ event . preventDefault ()
+}
+
+return (
+ < form onSubmit = { handleSubmit }>
+ { /* ... */ }
+ </ form >
+)
+
import { SyntheticEvent } from 'react' // 記得 import
+
+// ...
+
+const handleSubmit = ( event : SyntheticEvent ) => {
+ event . preventDefault ()
+}
+
+return (
+ < form onSubmit = { handleSubmit }>
+ { /* ... */ }
+ </ form >
+)
+
分析 考慮以下處理表單送出的程式碼:
function App () {
+ return (
+ < form
+ onSubmit = {( event ) => {
+ event . preventDefault ()
+ /* ... */
+ }}
+ >
+ { /* ... */ }
+ </ form >
+ )
+}
+
function App () {
+ return (
+ < form
+ onSubmit = {( event ) => {
+ event . preventDefault ()
+ /* ... */
+ }}
+ >
+ { /* ... */ }
+ </ form >
+ )
+}
+
有時候會想把 inline event handler onSubmit
給抽出來,變成 handleSubmit
。
但直接取出函式當然是行不通的,因為 TypeScript 無從推斷出參數 event
的型別。
function App () {
+ const handleSubmit = ( event ) => {
+ // ^^^^^
+ // Parameter 'event' implicitly has an 'any' type.ts(7006)
+ event . preventDefault ()
+ }
+
+ return (
+ < form onSubmit = { handleSubmit }>
+ { /* ... */ }
+ </ form >
+ )
+}
+
function App () {
+ const handleSubmit = ( event ) => {
+ // ^^^^^
+ // Parameter 'event' implicitly has an 'any' type.ts(7006)
+ event . preventDefault ()
+ }
+
+ return (
+ < form onSubmit = { handleSubmit }>
+ { /* ... */ }
+ </ form >
+ )
+}
+
在某些情況,如果 handler 裡根本沒用到 event
這個參數,例如常用的 onClick
事件,那麼這時其實把參數直接移除就行。
const handleClick = () => {
+ console . log ( 'Clicked' )
+}
+
+return (
+ < button onClick = { handleClick }>
+ Button
+ </ button >
+)
+
const handleClick = () => {
+ console . log ( 'Clicked' )
+}
+
+return (
+ < button onClick = { handleClick }>
+ Button
+ </ button >
+)
+
然而在處理表單的 onSubmit
的場合時,經常會需要呼叫這個事件的 preventDefault()
,用來阻止事件傳遞。
因此我們常常得 「復刻」 出這個事件的型別,而不得不寫出冗長難讀的型別,例如 React.FormEventHandler<HTMLFormElement>
。
function App () {
+ const handleSubmit : React . FormEventHandler < HTMLFormElement > = ( event ) => {
+ event . preventDefault ()
+ }
+
+ return (
+ < form onSubmit = { handleSubmit }>
+ { /* ... */ }
+ </ form >
+ )
+}
+
function App () {
+ const handleSubmit : React . FormEventHandler < HTMLFormElement > = ( event ) => {
+ event . preventDefault ()
+ }
+
+ return (
+ < form onSubmit = { handleSubmit }>
+ { /* ... */ }
+ </ form >
+ )
+}
+
BTW 這一長串型別的定義,可以透過將游標放在 JSX 事件上得知。
Synthetic Event 翻開 React 的官方文件 可以得知,在 React 中的所有事件都是 Synthetic Event ,而非 原生的 Event。兩者的 API 雖然長得很相似,但實際上並不是同一個東西。
可以試著追溯 onSubmit
事件的型別來觀察這件事:
// node_modules/@types/react/index.d.ts:1383
+interface DOMAttributes < T > {
+ // ...
+ onSubmit ?: FormEventHandler < T > | undefined ;
+}
+
// node_modules/@types/react/index.d.ts:1383
+interface DOMAttributes < T > {
+ // ...
+ onSubmit ?: FormEventHandler < T > | undefined ;
+}
+
再往上查看 FormEventHandler
和 FormEvent
:
// node_modules/@types/react/index.d.ts:1303
+type FormEventHandler < T = Element > = EventHandler < FormEvent < T >>;
+
+// node_modules/@types/react/index.d.ts:1195
+interface FormEvent < T = Element > extends SyntheticEvent < T > {
+}
+
// node_modules/@types/react/index.d.ts:1303
+type FormEventHandler < T = Element > = EventHandler < FormEvent < T >>;
+
+// node_modules/@types/react/index.d.ts:1195
+interface FormEvent < T = Element > extends SyntheticEvent < T > {
+}
+
由此可知 onSubmit
確實是一層層地從 SyntheticEvent
extend 出來的。
可以再進一步查看 SyntheticEvent
型別的定義,即可看到 preventDefault()
方法:
interface SyntheticEvent < T = Element , E = Event >
+ extends BaseSyntheticEvent < E , EventTarget & T , EventTarget > {}
+
+interface BaseSyntheticEvent < E = object , C = any , T = any > {
+ nativeEvent : E ;
+ currentTarget : C ;
+ target : T ;
+ bubbles : boolean ;
+ cancelable : boolean ;
+ defaultPrevented : boolean ;
+ eventPhase : number ;
+ isTrusted : boolean ;
+ preventDefault () : void ;
+ isDefaultPrevented () : boolean ;
+ stopPropagation () : void ;
+ isPropagationStopped () : boolean ;
+ persist () : void ;
+ timeStamp : number ;
+ type : string ;
+}
+
interface SyntheticEvent < T = Element , E = Event >
+ extends BaseSyntheticEvent < E , EventTarget & T , EventTarget > {}
+
+interface BaseSyntheticEvent < E = object , C = any , T = any > {
+ nativeEvent : E ;
+ currentTarget : C ;
+ target : T ;
+ bubbles : boolean ;
+ cancelable : boolean ;
+ defaultPrevented : boolean ;
+ eventPhase : number ;
+ isTrusted : boolean ;
+ preventDefault () : void ;
+ isDefaultPrevented () : boolean ;
+ stopPropagation () : void ;
+ isPropagationStopped () : boolean ;
+ persist () : void ;
+ timeStamp : number ;
+ type : string ;
+}
+
結論 經過分析可以得知,其實以下任一種寫法都可以使 handleSubmit
相容 onSubmit
事件。
const handleSubmit = ( e : React . SyntheticEvent ) => {
+ e . preventDefault ()
+}
+
const handleSubmit = ( e : React . SyntheticEvent ) => {
+ e . preventDefault ()
+}
+
const handleSubmit = ( e : React . FormEvent ) => {
+ e . preventDefault ()
+}
+
const handleSubmit = ( e : React . FormEvent ) => {
+ e . preventDefault ()
+}
+
const handleSubmit : React . FormEventHandler = ( e ) => {
+ e . preventDefault ()
+}
+
const handleSubmit : React . FormEventHandler = ( e ) => {
+ e . preventDefault ()
+}
+
+
+
+
+
\ No newline at end of file
diff --git a/blog/reproduce-bootstrap-grid-in-tailwind.html b/blog/reproduce-bootstrap-grid-in-tailwind.html
new file mode 100644
index 00000000..ca702a11
--- /dev/null
+++ b/blog/reproduce-bootstrap-grid-in-tailwind.html
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 用 Tailwind 重現 Bootstrap 的 Grid System | ngseke
+
+ 用 Tailwind 重現 Bootstrap 的 Grid System Aug 22, 2022 by Sean Huang 此筆記提到的 Grid 指的是元件庫 Bootstrap 所提供的的網格系統(Grid system),和 CSS 的 display: grid
屬性無關。
轉換後的 class 屬性有稍微簡化,但我想足以覆蓋大部分的使用場景了。
Bootstrap 和 Tailwind 的 Class Name 對應 row
→ flex flex-wrap
col
→ flex-1
備註:
.flex-1
+ flex : 1 // 即 `1 1 0%`
+
.flex-1
+ flex : 1 // 即 `1 1 0%`
+
col-auto
→ flex-none
備註:
.flex-none
+ flex : none // 即 `0 0 auto`
+
.flex-none
+ flex : none // 即 `0 0 auto`
+
col-*
col-1
→ flex-none w-1/12
col-3
→ flex-none w-1/4
col-4
→ flex-none w-1/3
col-6
→ flex-none w-1/2
col-12
→ flex-none w-full
有需要的話,你甚至可以在 Tailwind 透過 Arbitrary Value 的特性來指定任意寬度,例如:w-[5rem]
、w-[50px]
。
w-100
→ w-full
用於強迫換行。
響應式(RWD) By default, Tailwind uses a mobile first breakpoint system, … – Breakpoints · Bootstrap v5.2
Mobile first , responsive design is the goal. … – Responsive Design - Tailwind CSS
Tailwind 和 Bootstrap 都同樣遵循著行動裝置優先 的設計哲學,寬度由小到大。也就是說,當什麼斷點都沒加的時候,樣式會套用到所有寬度的裝置(>= 0px
),而加上 sm
時會套用在 >= 640px
,再加上 md
時會套用在 >= 768px
,依此類推。
值得注意的是,它們兩者斷點的預設值稍微不太一樣,例如 sm
在 Tailwind 是 640px
,在 Bootstrap 則是 576px
。不過這完全不成問題,因為你可以在 tailwind.config.js
輕鬆的自定義 ,甚至自創斷點名稱。
轉換範例如下:
col-12
→ flex-none w-full
col-md-6
→ flex-none md:w-1/2
col-3 col-sm-auto col-md col-lg-12
→flex-1 w-1/4 sm:flex-none sm:w-auto md:flex-1 lg:flex-none lg:w-full
間距(Gutter) 在預設情況下,column 之間是沒有間距的。這相當於是套用了 Bootstrap 4 的 row no-gutters
,或是 Bootstrap 5 的 row g-0
。
在 Tailwind 的場合想要實現 gutter,必須另外做以下兩點:
為 row 元素加上負值 的水平 margin,例如 -mx-4
。 為每一個 column 元素,加上與 row 元素相對應的水平 padding,例如 px-4
。 row 的負值 margin 是用來抵銷在最左或最右邊緣的 column 的 padding。
範例 < div class = "-mx-4 flex flex-wrap" >
+ < div class = "w-full flex-none px-4" >
+ col-12
+ </ div >
+ < div class = "w-1/2 flex-none px-4" >
+ col-6
+ </ div >
+ < div class = "w-1/4 flex-none px-4" >
+ col-3
+ </ div >
+ < div class = "w-full" > <!-- 換行用 --> </ div >
+ < div class = "flex-none px-4" >
+ col
+ </ div >
+ < div class = "flex-1 px-4" >
+ col-auto
+ </ div >
+</ div >
+
< div class = "-mx-4 flex flex-wrap" >
+ < div class = "w-full flex-none px-4" >
+ col-12
+ </ div >
+ < div class = "w-1/2 flex-none px-4" >
+ col-6
+ </ div >
+ < div class = "w-1/4 flex-none px-4" >
+ col-3
+ </ div >
+ < div class = "w-full" > <!-- 換行用 --> </ div >
+ < div class = "flex-none px-4" >
+ col
+ </ div >
+ < div class = "flex-1 px-4" >
+ col-auto
+ </ div >
+</ div >
+
延伸閱讀 flex-basis
和 width
的差別是什麼?
+
+
+
+
\ No newline at end of file
diff --git a/blog/typescript-as-const.html b/blog/typescript-as-const.html
new file mode 100644
index 00000000..cb0c0d73
--- /dev/null
+++ b/blog/typescript-as-const.html
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 讓 TypeScript 的 as const 救你一命 | ngseke
+
+ 讓 TypeScript 的 as const 救你一命 Dec 03, 2021 by Sean Huang 這篇文章討論的是 TypeScript 特有的「常數斷言(Const Assertion)」語法, 和 JavaScript 宣告常數所使用的 const foo = 1
是不同東西。
情境一:常數字串陣列 情境描述 🎡 TS Playground
想像我正在建立一個切換難易度的功能,有三種難度可選,先將他們宣告成陣列。
const options = [ 'easy' , 'normal' , 'hard' ]
+
const options = [ 'easy' , 'normal' , 'hard' ]
+
接著再建立一個變數 difficulty
,用來記錄目前選中的難易度。
type Difficulty = typeof options [ number ] // string
+
+let difficulty : Difficulty = options [ 0 ]
+
type Difficulty = typeof options [ number ] // string
+
+let difficulty : Difficulty = options [ 0 ]
+
但仔細一看型別 Difficulty
居然是 string
,這也太隨便了,因為這代表我可以把 difficulty
設成隨便一個字串,TypeScript 也不會阻止我。
difficulty = '安安' // 😑
+
difficulty = '安安' // 😑
+
解法 🎡 TS Playground
這時 as const
(const assertion)就派上用場了!寫法就像型別斷言 那樣。
const options = [ 'easy' , 'normal' , 'hard' ] as const
+
+// 或是使用尖括弧(tsx 以外的檔案才能用這種寫法)
+const options = < const >[ 'easy' , 'normal' , 'hard' ]
+
const options = [ 'easy' , 'normal' , 'hard' ] as const
+
+// 或是使用尖括弧(tsx 以外的檔案才能用這種寫法)
+const options = < const >[ 'easy' , 'normal' , 'hard' ]
+
如此即可得到預期中準確的型別。
type Difficulty = typeof difficulties [ number ] // "easy" | "normal" | "hard"
+
type Difficulty = typeof difficulties [ number ] // "easy" | "normal" | "hard"
+
搭配 VSCode 的 IntelliSense 可以得到代碼提示,也不再怕手殘拼錯字。(快速鍵 ⌘ + I
呼出)
map()
等回調函式的參數也會自動推斷出型別。
// 假設有另一個函式參數要求傳入 `Difficulty`
+const foo = ( difficulty : Difficulty ) => { /* ... */ }
+
+// difficulty: "easy" | "normal" | "hard"
+options . map ( difficulty => foo ( difficulty ))
+
// 假設有另一個函式參數要求傳入 `Difficulty`
+const foo = ( difficulty : Difficulty ) => { /* ... */ }
+
+// difficulty: "easy" | "normal" | "hard"
+options . map ( difficulty => foo ( difficulty ))
+
當然你也可以一開始就先把型別定義好,再以型別約束變數。 但我個人通常不太這麼做,因為這樣會重複出現很雷同的代碼,相當於需要費力維護多個真相來源。例如:
type Difficulty = 'easy' | 'normal' | 'hard'
+const options : Difficulty [] = [ 'easy' , 'normal' , 'hard' ]
+
type Difficulty = 'easy' | 'normal' | 'hard'
+const options : Difficulty [] = [ 'easy' , 'normal' , 'hard' ]
+
不過把型別抽出來單獨定義,還是有額外好處的。假設因為需求改變,需要把 'easy'
改名成 'simple'
,就可以活用 VSCode 的 重新命名符號(Rename Symbol) 功能來快速重構(從右鍵選單或快速鍵 F2
呼出)。
而這便是 as const
無法做到的,因此建議還是根據實際使用場景做權衡。
情境二:日期區間的 tuple 情境描述 🎡 TS Playground
想像我正在建立一個從開始到結束的時間區間 變數 range
,一樣宣告成陣列。
const range = [ new Date (), new Date ()]
+
const range = [ new Date (), new Date ()]
+
還有一個參數是時間區間的函式。
function foo ( range : [ Date , Date ]) { /* ... */ }
+
function foo ( range : [ Date , Date ]) { /* ... */ }
+
接著試著把 range
放入函式 foo()
中,但卻出現型別不相容的錯誤。
foo ( range )
+// Argument of type 'Date[]' is not assignable to parameter of type '[Date, Date]'.
+// Target requires 2 element(s) but source may have fewer.(2345)
+
foo ( range )
+// Argument of type 'Date[]' is not assignable to parameter of type '[Date, Date]'.
+// Target requires 2 element(s) but source may have fewer.(2345)
+
分析跳出的錯誤得知:Date[]
不可指派給 [Date, Date]
,因為函式只接受長度剛好是 2
的陣列,但 range
卻可能是任何長度的陣列。
TypeScript 說的確實沒錯,即使原本 range
是用 const
宣告,但我在中途還是有機會偷改他。例如 push 新的東西進去,或是執行 range.length = 0
來清空陣列,這些都會改變陣列的長度。
解法 🎡 TS Playground
試著加上 as const
。
const range = [ new Date (), new Date ()] as const
+
const range = [ new Date (), new Date ()] as const
+
檢查 range
的型別,會發現它從原本的 Date[]
變成 readonly [Date, Date]
了。
type MyRange = typeof range // readonly [23, 28]
+
type MyRange = typeof range // readonly [23, 28]
+
此後便無法再對 range
進行任何會改變它的操作了。
range . length = 0 // 🚫
+range . push ( new Date ()) // 🚫
+range . reverse () // 🚫
+
range . length = 0 // 🚫
+range . push ( new Date ()) // 🚫
+range . reverse () // 🚫
+
接著即可成功將 range
傳入函式。
const range = [ new Date (), new Date ()] as const
+
+function foo ( range : readonly [ Date , Date ]) { /* ... */ }
+
+foo ( range ) // ✅
+
const range = [ new Date (), new Date ()] as const
+
+function foo ( range : readonly [ Date , Date ]) { /* ... */ }
+
+foo ( range ) // ✅
+
留意函式的參數型別多加上了 readonly
關鍵字,那是用來告訴 TypeScript,這個函式不會去動到 range
,例如執行 range[0] = ...
來重新賦值陣列的某個項目。
而試圖存取超出陣列範圍的項目時,TypeScript 也會提示錯誤。(JavaScript 的情況則是會無聲地得到 undefined
)
// ❌ Tuple type '[Date, Date]' of length '2' has no element at index '100'.(2493)
+range [ 100 ]
+
// ❌ Tuple type '[Date, Date]' of length '2' has no element at index '100'.(2493)
+range [ 100 ]
+
甚至可以很明確地得知參數 length
的型別為 2
type Length = typeof range . length // 2
+
type Length = typeof range . length // 2
+
總結 as const
套用在不同型別的變數上會得到不同的效果:
string、number、boolean 字面型別(literal type)加上 as const
後,型別就不會被「拓寬」,例如字串 'hello'
不會被推斷成 string
,而是會維持原樣。
let a = 'hello' // string
+let b = 'hello' as const // 'hello'
+
+let c = 123 // number
+let d = 123 as const // 123
+
+let e = true // boolean
+let f = true as const // true
+
let a = 'hello' // string
+let b = 'hello' as const // 'hello'
+
+let c = 123 // number
+let d = 123 as const // 123
+
+let e = true // boolean
+let f = true as const // true
+
陣列 陣列會被轉換成 readonly 的 tuple,也就是:
唯獨,陣列裡面的值始終相同,也不能被修改 長度永遠固定,不能執行 push()
或 pop()
等操作 let a = [ 123 , 'hello' ] // (string | number)[]
+let b = [ 123 , 'hello' ] as const // readonly [123, 'hello']
+
let a = [ 123 , 'hello' ] // (string | number)[]
+let b = [ 123 , 'hello' ] as const // readonly [123, 'hello']
+
物件 物件裡的所有 屬性都會被加上 readonly,並且裡面的 string、number、boolean 和陣列值都會比照上述處理,型別不會被拓寬。
let a = { text : 'hello' , nested : { count : 123 } }
+// {
+// text: string
+// nested: { count: number }
+// }
+
+let b = { text : 'hello' , nested : { count : 123 } } as const
+// {
+// readonly text: "hello"
+// readonly nested: { readonly count: 123; }
+// }
+
let a = { text : 'hello' , nested : { count : 123 } }
+// {
+// text: string
+// nested: { count: number }
+// }
+
+let b = { text : 'hello' , nested : { count : 123 } } as const
+// {
+// readonly text: "hello"
+// readonly nested: { readonly count: 123; }
+// }
+
參考資料 https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions
+
+
+
+
\ No newline at end of file
diff --git a/blog/vite-vue-ts-eslint-setup.html b/blog/vite-vue-ts-eslint-setup.html
new file mode 100644
index 00000000..ca9de367
--- /dev/null
+++ b/blog/vite-vue-ts-eslint-setup.html
@@ -0,0 +1,188 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 建立 Vite + Vue + TypeScript + ESLint 專案可能會遇到的坑 | ngseke
+
+ 建立 Vite + Vue + TypeScript + ESLint 專案可能會遇到的坑 Oct 03, 2023 by Sean Huang 以下使用 pnpm 作為套件管理工具,與 npm 或 yarn 的指令會有些許差異,請查看隨附的官方文件
TL;DR 完整配置範例:https://github.com/ngseke/vite-vue-ts-eslint-example
🏗️ 透過 Vite 官方的 Preset 新建專案 Scaffolding Your First Vite Project
pnpm create vite my-vue-app --template vue-ts
+
pnpm create vite my-vue-app --template vue-ts
+
移動到該目錄並安裝相依
cd my-vue-app
+pnpm i
+
cd my-vue-app
+pnpm i
+
🏗️ 初始化 ESLint Getting Started with ESLint - Quick start
pnpm create @eslint/config
+
pnpm create @eslint/config
+
依序回答以下問題後,便會在根目錄新建 .eslintrc.cjs
:
✔ How would you like to use ESLint? · style ✔ What type of modules does your project use? · esm ✔ Which framework does your project use? · vue ✔ Does your project use TypeScript? · No / Yes ✔ Where does your code run? · browser ✔ How would you like to define a style for your project? · guide ✔ Which style guide do you want to follow? · standard-with-typescript ✔ What format do you want your config file to be in? · JavaScript ✔ Would you like to install them now? · No / Yes ✔ Which package manager do you want to use? · pnpm
😵 處理 VSCode Output 的 ESLint 的問題 雖然 ESLint 已初始化完成,但你會發現它尚未正常運作。例如試著在任意 .ts
或 .vue
檔隨便加多餘的空格,卻看不到預期的 error 或 warning 的波浪底線。
查看 Output 會看到以下錯誤訊息:
An unexpected error occurred: Error: Error while loading rule ‘@typescript-eslint/dot-notation’: You have used a rule which requires parserServices to be generated. You must therefore provide a value for the “parserOptions.project” property for @typescript-eslint/parser.
根據錯誤訊息的描述,得知需要在 .eslintrc.cjs
補上 parserOptions.project
並指明 parserOptions.parser
。
// .eslintrc.cjs
+module.exports = {
+ // ...
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module",
++ project: ['./tsconfig.json', './tsconfig.node.json'],
++ parser: '@typescript-eslint/parser',
+ },
+}
+
// .eslintrc.cjs
+module.exports = {
+ // ...
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module",
++ project: ['./tsconfig.json', './tsconfig.node.json'],
++ parser: '@typescript-eslint/parser',
+ },
+}
+
再次查看 Output 錯誤訊息已消失
Q: 為什麼是 parserOptions.parser
而非 parser
? A: 根據官方文件 說明,若寫在 parser
會把 vue-eslint-parser
覆蓋掉而無法正常地 lint .vue
檔。所以當有自訂的 parser 時(例如 @typescript-eslint/parser
),必須把它移入 parserOptions
。
😵 處理根目錄檔 TSConfig include 的問題 雖然 Output 錯誤訊息已消失,但這時用 VSCode 開啟在根目錄的 .ts
和 .cjs
檔,例如 vite.config.ts
和.eslintrc.cjs
,會發現它們在開頭都多了紅色的波浪底線:
Parsing error: ESLint was configured to run on <tsconfigRootDir>/.eslintrc.cjs
using parserOptions.project
: /users/sean/my-vue-app/tsconfig.json However, that TSConfig does not include this file. Either:
根據錯誤訊息得知,我們必須在 tsconfig.node.json
的 includes
中手動加入這些檔案:
// tsconfig.node.json
+{
+ // ...
+ "include": [
+ "vite.config.ts",
++ ".eslintrc.cjs"
+ ],
+}
+
// tsconfig.node.json
+{
+ // ...
+ "include": [
+ "vite.config.ts",
++ ".eslintrc.cjs"
+ ],
+}
+
日後若是在根目錄有新增那些在 Node 環境 執行的,而不會被實際打包進專案的設定檔,例如 Tailwind CSS 的設定檔 tailwind.config.ts
,也要記得手動加入進去。
接著重新載入 VSCode 視窗
叫出指令視窗 ⌘ + shift + P
→ 輸入 Developer: Reload Window
打開 .eslintrc.cjs
可以看到 ESLint 終於正常運作了,它開始確實根據 eslint-config-standard-with-typescript 的預設規則進行檢查,例如多餘的引號、字串應為單引號和縮排應為 2 格空格等。
😵 處理 .vue
檔的 non-standard 問題 打開任意 .vue
檔會發現它們也出現了錯誤訊息:
Parsing error: ESLint was configured to run on <tsconfigRootDir>/src/App.vue
using parserOptions.project
: /users/sean/my-vue-app/tsconfig.json The extension for the file (.vue
) is non-standard. You should add parserOptions.extraFileExtensions
to your config.
根據錯誤訊息的提示在 .eslintrc.cjs
加入 parserOptions.extraFileExtensions
後,再次重新載入 VSCode 視窗即可。
// .eslintrc.cjs
+{
+ // ...
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module",
+ project: ['./tsconfig.json', './tsconfig.node.json'],
+ parser: '@typescript-eslint/parser',
++ extraFileExtensions: ['.vue']
+ },
+}
+
// .eslintrc.cjs
+{
+ // ...
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module",
+ project: ['./tsconfig.json', './tsconfig.node.json'],
+ parser: '@typescript-eslint/parser',
++ extraFileExtensions: ['.vue']
+ },
+}
+
😵 跳過檢查某些檔案 dist/
我們沒必要檢查建構好的已醜化和壓縮的檔案,因此可以在 .eslintrc.cjs
的 ignorePatterns
排除掉整個 dist
目錄:
// .eslintrc.cjs
+module.exports = {
+ // ...
++ ignorePatterns: ['dist'],
+}
+
// .eslintrc.cjs
+module.exports = {
+ // ...
++ ignorePatterns: ['dist'],
+}
+
vite-env.d.ts
按照預設規則,vite-env.d.ts
會違反 @typescript-eslint/triple-slash-reference
這條規則,建議可以在 vite-env.d.ts
的開頭加上註解 // eslint-disable-next-line ...
來跳過檢查:
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
+/// < reference types = "vite/client" />
+
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
+/// < reference types = "vite/client" />
+
✅ 大功告成 到此為止 ESLint 應該就可以順利的運作,接著你可以更進一步根據個人或團隊的風格和偏好調整 rules
,開啟、關閉或調整某些規則。
例如我總是習慣微調 @typescript-eslint/comma-dangle (行末逗號)的設定,讓最後一項依然保留逗號,這樣就可以更方便調整物件成員的順序:
module . exports = {
+ // ...
+ rules : {
+ // ...
+ '@typescript-eslint/comma-dangle' : [ 'error' , {
+ arrays : 'always-multiline' ,
+ objects : 'always-multiline' ,
+ imports : 'always-multiline' ,
+ exports : 'always-multiline' ,
+ functions : 'only-multiline' ,
+ }],
+ },
+}
+
module . exports = {
+ // ...
+ rules : {
+ // ...
+ '@typescript-eslint/comma-dangle' : [ 'error' , {
+ arrays : 'always-multiline' ,
+ objects : 'always-multiline' ,
+ imports : 'always-multiline' ,
+ exports : 'always-multiline' ,
+ functions : 'only-multiline' ,
+ }],
+ },
+}
+
✨ 設定 VSCode 存檔時自動排版 你還可以讓開發體驗變的更舒適。
打開 .vscode/settings.json
,若原本沒有這個檔案可以手動建立一個,加入以下設定:
{
+ // ...
+ "editor.codeActionsOnSave" : {
+ "source.fixAll.eslint" : true
+ },
+}
+
{
+ // ...
+ "editor.codeActionsOnSave" : {
+ "source.fixAll.eslint" : true
+ },
+}
+
回到剛才滿江紅的 .eslintrc.cjs
測試看看,在手動存檔(⌘ + S
)後就會自動排版和修正錯誤:
✨ 透過 script 檢查錯誤或是自動修復錯誤 儘管在 VSCode 中的檢查和錯誤提示有助於得到即時反饋,但這些提示終究是 「消極的」 。如果開發者使用別款 IDE,或只是單純對提示視若無睹,他們仍然可以輕易地違反這些規則,並提交不符合規則的程式碼。
因此我們還需要透過 script 的方式來真正的執行規則檢查。
用指令檢查錯誤 打開 package.json
,在 scripts
中加入指令 lint
,其中 --ext
的後面是想要檢查的副檔名:
// package.json
+{
+ // ...
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc && vite build",
+ "preview": "vite preview",
++ "lint": "npx eslint . --ext .ts,.js,.cjs,.vue"
+ },
+}
+
// package.json
+{
+ // ...
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc && vite build",
+ "preview": "vite preview",
++ "lint": "npx eslint . --ext .ts,.js,.cjs,.vue"
+ },
+}
+
接著在 terminal 測試執行效果
pnpm run lint
+
pnpm run lint
+
可以看到它列出了所有不符合設定規則的 error 和 warning,並且回傳 exit code 1
。
這表示你就可以將這條指令整合進你的部署流程中,例如:
搭配 husky :使用 husky 可以在每次 commit 前自動執行 pnpm run lint
。如果程式碼不符合規則就不給 commit,迫使開發者修復錯誤後再進行 commit,確保 code base 的風格始終維持一致CI/CD :你可以將 pnpm run lint
加入 CI/CD 流程,這樣在每次部署前都會自動進行代碼檢查。若檢查失敗,部署流程就會自動中斷,讓開發者不得不修正錯誤用指令自動修復錯誤 打開 package.json
,在 scripts
中加入以下指令 lint:fix
:
// package.json
+{
+ // ...
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc && vite build",
+ "preview": "vite preview",
+ "lint": "npx eslint . --ext .ts,.js,.cjs,.vue",
++ "lint:fix": "npx eslint --fix . --ext .ts,.js,.cjs,.vue"
+ },
+}
+
// package.json
+{
+ // ...
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc && vite build",
+ "preview": "vite preview",
+ "lint": "npx eslint . --ext .ts,.js,.cjs,.vue",
++ "lint:fix": "npx eslint --fix . --ext .ts,.js,.cjs,.vue"
+ },
+}
+
接著在 terminal 測試執行效果
pnpm run lint:fix
+
pnpm run lint:fix
+
順利的話「可自動修正 (automatically fixable)」的那些規則都會被自動修正,也就是在 Rule 列表 有 🔧 符號的那些項目。
ESLint 即將棄用「排版」規則 ESLint 在 2023 年 10 月宣布將棄用排版(Formatting)規則 ,也就是棄用那些跟空格、縮排、換行、單/雙引號、分號等 相關規則。而其餘分類下的規則不受影響,例如強制使用嚴格等於 ===
(eqeqeq )、強制命名小駝峰(camelcase )等。
未來若想繼續透過 ESLint 而非 Prettier 來排版程式碼,可以考慮搭配 ESLint Stylistic 來達成一樣的效果。這個 plugin 將會繼續接棒,維護這些被棄用的規則。
參考資料 https://github.com/vitejs/vite/issues/13739#issuecomment-1641380518 https://juejin.cn/post/7126043888573218823
+
+
+
+
\ No newline at end of file
diff --git a/blog/webpack-config-esm.html b/blog/webpack-config-esm.html
new file mode 100644
index 00000000..d54a95b7
--- /dev/null
+++ b/blog/webpack-config-esm.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 在 webpack.config.js 裡使用 ESM 的 import 語法 | ngseke
+
+ 在 webpack.config.js 裡使用 ESM 的 import 語法 Feb 20, 2022 by Sean Huang 步驟 安裝 @babel/register
與 babel-preset-env
npm install -D @babel/core @babel/register babel-preset-env
+
npm install -D @babel/core @babel/register babel-preset-env
+
設定 .babelrc
{
+ "presets": [
++ "@babel/preset-env"
+ ],
+}
+
+
{
+ "presets": [
++ "@babel/preset-env"
+ ],
+}
+
+
更新 webpack 設定 將原 webpack.config.js
更名為 webpack.config.babel.js
在專案根目錄建立 webpack.config.js
require ( '@babel/register' )
+module . exports = require ( './webpack.config.babel.js' )
+
require ( '@babel/register' )
+module . exports = require ( './webpack.config.babel.js' )
+
參考資料
+
+
+
+
\ No newline at end of file
diff --git a/blog/wikipedia-link-converter.html b/blog/wikipedia-link-converter.html
new file mode 100644
index 00000000..aa883c44
--- /dev/null
+++ b/blog/wikipedia-link-converter.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 還給你 Google 搜尋結果的桌面版維基百科 | ngseke
+
+ 還給你 Google 搜尋結果的桌面版維基百科 May 27, 2022 by Sean Huang
+
+
+
+
\ No newline at end of file
diff --git a/favicon.png b/favicon.png
new file mode 100755
index 00000000..55cf646b
Binary files /dev/null and b/favicon.png differ
diff --git a/fe.html b/fe.html
new file mode 100644
index 00000000..94647fce
--- /dev/null
+++ b/fe.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/files/TypingTyping.zip b/files/TypingTyping.zip
new file mode 100644
index 00000000..5cf5577c
Binary files /dev/null and b/files/TypingTyping.zip differ
diff --git a/img/og/blog-bootstrap-in-nuxt.png b/img/og/blog-bootstrap-in-nuxt.png
new file mode 100644
index 00000000..4eb7bf85
Binary files /dev/null and b/img/og/blog-bootstrap-in-nuxt.png differ
diff --git a/img/og/blog-check-if-key-exists.png b/img/og/blog-check-if-key-exists.png
new file mode 100644
index 00000000..39de97ed
Binary files /dev/null and b/img/og/blog-check-if-key-exists.png differ
diff --git a/img/og/blog-code-review-taxi-number.png b/img/og/blog-code-review-taxi-number.png
new file mode 100644
index 00000000..84226f90
Binary files /dev/null and b/img/og/blog-code-review-taxi-number.png differ
diff --git a/img/og/blog-component-naming.png b/img/og/blog-component-naming.png
new file mode 100644
index 00000000..e41fb8c6
Binary files /dev/null and b/img/og/blog-component-naming.png differ
diff --git a/img/og/blog-customize-youtube-caption-font.png b/img/og/blog-customize-youtube-caption-font.png
new file mode 100644
index 00000000..74738414
Binary files /dev/null and b/img/og/blog-customize-youtube-caption-font.png differ
diff --git a/img/og/blog-react-handler-type.png b/img/og/blog-react-handler-type.png
new file mode 100644
index 00000000..ca47ac4b
Binary files /dev/null and b/img/og/blog-react-handler-type.png differ
diff --git a/img/og/blog-reproduce-bootstrap-grid-in-tailwind.png b/img/og/blog-reproduce-bootstrap-grid-in-tailwind.png
new file mode 100644
index 00000000..7ea01b28
Binary files /dev/null and b/img/og/blog-reproduce-bootstrap-grid-in-tailwind.png differ
diff --git a/img/og/blog-typescript-as-const.png b/img/og/blog-typescript-as-const.png
new file mode 100644
index 00000000..bb19b06d
Binary files /dev/null and b/img/og/blog-typescript-as-const.png differ
diff --git a/img/og/blog-vite-vue-ts-eslint-setup.png b/img/og/blog-vite-vue-ts-eslint-setup.png
new file mode 100644
index 00000000..22397d7d
Binary files /dev/null and b/img/og/blog-vite-vue-ts-eslint-setup.png differ
diff --git a/img/og/blog-webpack-config-esm.png b/img/og/blog-webpack-config-esm.png
new file mode 100644
index 00000000..295c4d2e
Binary files /dev/null and b/img/og/blog-webpack-config-esm.png differ
diff --git a/img/og/blog-wikipedia-link-converter.png b/img/og/blog-wikipedia-link-converter.png
new file mode 100644
index 00000000..1072f904
Binary files /dev/null and b/img/og/blog-wikipedia-link-converter.png differ
diff --git a/img/og/project-boss.png b/img/og/project-boss.png
new file mode 100644
index 00000000..54316e5f
Binary files /dev/null and b/img/og/project-boss.png differ
diff --git a/img/og/project-camp-2017.png b/img/og/project-camp-2017.png
new file mode 100644
index 00000000..7630f308
Binary files /dev/null and b/img/og/project-camp-2017.png differ
diff --git a/img/og/project-camp2017.png b/img/og/project-camp2017.png
new file mode 100644
index 00000000..7630f308
Binary files /dev/null and b/img/og/project-camp2017.png differ
diff --git a/img/og/project-credit-card-calc.png b/img/og/project-credit-card-calc.png
new file mode 100644
index 00000000..50a6ffba
Binary files /dev/null and b/img/og/project-credit-card-calc.png differ
diff --git a/img/og/project-em-optimization-lab.png b/img/og/project-em-optimization-lab.png
new file mode 100644
index 00000000..c0eabfbe
Binary files /dev/null and b/img/og/project-em-optimization-lab.png differ
diff --git a/img/og/project-emo.png b/img/og/project-emo.png
new file mode 100644
index 00000000..c0eabfbe
Binary files /dev/null and b/img/og/project-emo.png differ
diff --git a/img/og/project-flag.png b/img/og/project-flag.png
new file mode 100644
index 00000000..b84bfd32
Binary files /dev/null and b/img/og/project-flag.png differ
diff --git a/img/og/project-gomoku.png b/img/og/project-gomoku.png
new file mode 100644
index 00000000..eaea8517
Binary files /dev/null and b/img/og/project-gomoku.png differ
diff --git a/img/og/project-iphone-price.png b/img/og/project-iphone-price.png
new file mode 100644
index 00000000..e8a40dce
Binary files /dev/null and b/img/og/project-iphone-price.png differ
diff --git a/img/og/project-koasu.png b/img/og/project-koasu.png
new file mode 100644
index 00000000..ef90d163
Binary files /dev/null and b/img/og/project-koasu.png differ
diff --git a/img/og/project-leetcode-night.png b/img/og/project-leetcode-night.png
new file mode 100644
index 00000000..82d364c5
Binary files /dev/null and b/img/og/project-leetcode-night.png differ
diff --git a/img/og/project-mcip-cms.png b/img/og/project-mcip-cms.png
new file mode 100644
index 00000000..fce74b91
Binary files /dev/null and b/img/og/project-mcip-cms.png differ
diff --git a/img/og/project-mcip.png b/img/og/project-mcip.png
new file mode 100644
index 00000000..baa573b8
Binary files /dev/null and b/img/og/project-mcip.png differ
diff --git a/img/og/project-raise-your-red-flag.png b/img/og/project-raise-your-red-flag.png
new file mode 100644
index 00000000..b84bfd32
Binary files /dev/null and b/img/og/project-raise-your-red-flag.png differ
diff --git a/img/og/project-shanlinliang.png b/img/og/project-shanlinliang.png
new file mode 100644
index 00000000..83c26c47
Binary files /dev/null and b/img/og/project-shanlinliang.png differ
diff --git a/img/og/project-tic-tac-toe.png b/img/og/project-tic-tac-toe.png
new file mode 100644
index 00000000..c0a18e7d
Binary files /dev/null and b/img/og/project-tic-tac-toe.png differ
diff --git a/img/og/project-typing-typing.png b/img/og/project-typing-typing.png
new file mode 100644
index 00000000..e06231b1
Binary files /dev/null and b/img/og/project-typing-typing.png differ
diff --git a/img/og/project-typingtyping.png b/img/og/project-typingtyping.png
new file mode 100644
index 00000000..e06231b1
Binary files /dev/null and b/img/og/project-typingtyping.png differ
diff --git a/img/og/project-versatile-npm.png b/img/og/project-versatile-npm.png
new file mode 100644
index 00000000..d42fee16
Binary files /dev/null and b/img/og/project-versatile-npm.png differ
diff --git a/img/project-cover/boss.png b/img/project-cover/boss.png
new file mode 100755
index 00000000..cb3441c6
Binary files /dev/null and b/img/project-cover/boss.png differ
diff --git a/img/project-cover/camp2017.png b/img/project-cover/camp2017.png
new file mode 100755
index 00000000..9f087e61
Binary files /dev/null and b/img/project-cover/camp2017.png differ
diff --git a/img/project-cover/credit-card-calc.png b/img/project-cover/credit-card-calc.png
new file mode 100755
index 00000000..46004a40
Binary files /dev/null and b/img/project-cover/credit-card-calc.png differ
diff --git a/img/project-cover/emo.png b/img/project-cover/emo.png
new file mode 100755
index 00000000..84fc237b
Binary files /dev/null and b/img/project-cover/emo.png differ
diff --git a/img/project-cover/flag.png b/img/project-cover/flag.png
new file mode 100755
index 00000000..0d96deae
Binary files /dev/null and b/img/project-cover/flag.png differ
diff --git a/img/project-cover/flip-card.png b/img/project-cover/flip-card.png
new file mode 100755
index 00000000..ef943337
Binary files /dev/null and b/img/project-cover/flip-card.png differ
diff --git a/img/project-cover/gomoku.png b/img/project-cover/gomoku.png
new file mode 100755
index 00000000..0abc9d1e
Binary files /dev/null and b/img/project-cover/gomoku.png differ
diff --git a/img/project-cover/iphone-price.png b/img/project-cover/iphone-price.png
new file mode 100644
index 00000000..afc8c542
Binary files /dev/null and b/img/project-cover/iphone-price.png differ
diff --git a/img/project-cover/koasu.png b/img/project-cover/koasu.png
new file mode 100755
index 00000000..dc5053c7
Binary files /dev/null and b/img/project-cover/koasu.png differ
diff --git a/img/project-cover/leetcode-night.png b/img/project-cover/leetcode-night.png
new file mode 100755
index 00000000..e3606d86
Binary files /dev/null and b/img/project-cover/leetcode-night.png differ
diff --git a/img/project-cover/mcip-cms.png b/img/project-cover/mcip-cms.png
new file mode 100755
index 00000000..831337d6
Binary files /dev/null and b/img/project-cover/mcip-cms.png differ
diff --git a/img/project-cover/mcip.png b/img/project-cover/mcip.png
new file mode 100755
index 00000000..eeac6ece
Binary files /dev/null and b/img/project-cover/mcip.png differ
diff --git a/img/project-cover/shanlinliang.png b/img/project-cover/shanlinliang.png
new file mode 100755
index 00000000..3b9fc5c7
Binary files /dev/null and b/img/project-cover/shanlinliang.png differ
diff --git a/img/project-cover/taiwan-company-blocker.png b/img/project-cover/taiwan-company-blocker.png
new file mode 100644
index 00000000..3d74103a
Binary files /dev/null and b/img/project-cover/taiwan-company-blocker.png differ
diff --git a/img/project-cover/tic-tac-toe.png b/img/project-cover/tic-tac-toe.png
new file mode 100644
index 00000000..d9994e8e
Binary files /dev/null and b/img/project-cover/tic-tac-toe.png differ
diff --git a/img/project-cover/typing-typing.png b/img/project-cover/typing-typing.png
new file mode 100755
index 00000000..d2cdf6bc
Binary files /dev/null and b/img/project-cover/typing-typing.png differ
diff --git a/img/project-cover/versatile-npm.png b/img/project-cover/versatile-npm.png
new file mode 100644
index 00000000..0bbde45b
Binary files /dev/null and b/img/project-cover/versatile-npm.png differ
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..3efad8cd
--- /dev/null
+++ b/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ ngseke
+
+ @ngseke
Hi, I'm Sean . 🍻 現職前端軟體工程師,專注於 TypeScript 、Vue 和 React 。 追求撰寫無瑕程式碼是我的開發格言,並致力於優化開發者體驗。 熱衷探究前端領域的新鮮事,期待藉由知識分享為社群創造影響力。
aka 黃省喬 / HUANG Sing-Ciao
Skills React Vue 3 Tailwind CSS Nuxt TypeScript Node.js Experience Software Engineer @ ASUS AICS 2022 — 2023
Software Engineer @ 3drens 2021 — 2022
Software Engineer @ gogoout 2019 — 2021
Summer Engineering Intern @ Hiero7 Summer 2018
Projects Versatile Npm 自訂 Npm 安裝指令瀏覽器擴充功能
LeetCode Night LeetCode 深色模式瀏覽器擴充功能
+
+
+
+
\ No newline at end of file
diff --git a/project.html b/project.html
new file mode 100644
index 00000000..43f74522
--- /dev/null
+++ b/project.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Projects | ngseke
+
+ Browser Extension Taiwan Company Blocker 台灣求職網封鎖神器
Versatile Npm 自訂 Npm 安裝指令瀏覽器擴充功能
LeetCode Night LeetCode 深色模式瀏覽器擴充功能
Web iPhone Price 台灣 iPhone 價格歷史趨勢
MCIP Official Website 《樂台計畫》官方網站
Sinopac Dual Currency Card Calc. 永豐幣倍卡回饋計算機
EM Optimization Lab 《電磁最佳化實驗室》網站
System Design BOSS: Beverage Online Shop System 線上飲料購物系統
Game Raise Your Red Flag 以 Webcam 重現經典團康遊戲《紅旗舉起來》
Typing Typing! 8-bit 復古風格打字遊戲
Identity Design NTUT-CSIE & NTUB-ACC Camp, 2017 迎新《會炒不加辣,果資不加糖》品牌識別設計
Shanlinliang 虛構涼扇品牌廣告《扇林涼》
+
+
+
+
\ No newline at end of file
diff --git a/project/boss.html b/project/boss.html
new file mode 100644
index 00000000..42a33711
--- /dev/null
+++ b/project/boss.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ BOSS: Beverage Online Shop System | ngseke
+
+ BOSS: Beverage Online Shop System 線上飲料購物系統
Nov, 2017 - Jan, 2018 Team Member: 吳品頤、 余鎧企、 黃省喬 、 趙振廷 這是一個在線飲料購物系統 (Beverage Online Shop System),簡稱BOSS,是一個簡單易用,介面時尚清爽的飲料購物平台。本專案後端以PHP語言撰寫,前端以Bootstrap的架構搭配HTML5和CSS3作為基礎進行版型設計。
在此系統中可以根據不同使用者身份,包括顧客、員工及管理員,提供各種不同的操作功能。 我們提供簡單直覺易懂的操作界面,讓顧客可以瀏覽本店所有商品、根據分類或搜尋條件取得商品資訊、對於產品及訂單進行打分或評論、查看。 員工可以上下架商品、管理商品之庫存及資訊、查看當前所有訂單資訊。管理者可以新增移除或修改會員資訊。
介面
Demo http://boss.ngseke.me/ 已下架
+
+
+
+
\ No newline at end of file
diff --git a/project/camp-2017.html b/project/camp-2017.html
new file mode 100644
index 00000000..43bd5835
--- /dev/null
+++ b/project/camp-2017.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ NTUT-CSIE & NTUB-ACC Camp, 2017 | ngseke
+
+ NTUT-CSIE & NTUB-ACC Camp, 2017 迎新《會炒不加辣,果資不加糖》品牌識別設計
+
+
+
+
\ No newline at end of file
diff --git a/project/camp2017.html b/project/camp2017.html
new file mode 100644
index 00000000..43bd5835
--- /dev/null
+++ b/project/camp2017.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ NTUT-CSIE & NTUB-ACC Camp, 2017 | ngseke
+
+ NTUT-CSIE & NTUB-ACC Camp, 2017 迎新《會炒不加辣,果資不加糖》品牌識別設計
+
+
+
+
\ No newline at end of file
diff --git a/project/credit-card-calc.html b/project/credit-card-calc.html
new file mode 100644
index 00000000..8cc6ad8f
--- /dev/null
+++ b/project/credit-card-calc.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Sinopac Dual Currency Card Calc. | ngseke
+
+ Sinopac Dual Currency Card Calc. 永豐幣倍卡回饋計算機
Vue TypeScript Tailwind CSS Vite UI/UX
+
+
+
+
\ No newline at end of file
diff --git a/project/em-optimization-lab.html b/project/em-optimization-lab.html
new file mode 100644
index 00000000..9c1e4ead
--- /dev/null
+++ b/project/em-optimization-lab.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ EM Optimization Lab | ngseke
+
+ EM Optimization Lab 《電磁最佳化實驗室》網站
這是台北科技大學之電磁最佳化實驗室 (EM Optimization Lab) 的網站,包含 RWD 來增進不同裝置上的瀏覽體驗。
前端主要以 Vue 架構,使用 Sass 和 Pug 搭配 Gulp 自動化流程工具編譯,使用 Firebase 作為資料庫。
後台管理 網站提供了簡單易用的後台介面,讓管理者能方便快速地更新頁面內容,如修改首頁近期活動 、上傳輪播照片 、 修改實驗室成員 等。
考慮到網站部署空間的限制,因為學校提供的上傳空間只支援靜態網頁,所以資料庫選擇使用 Firebase ,其最大的特色就是只靠靜態的前端網頁就可以操作 database。此外它也可以靠 JS 來上傳檔案到儲存空間,也提供驗證登入的功能,讓擁有權限的使用者才可進入後台編輯。
Demo https://myweb.ntut.edu.tw/~yschen/
+
+
+
+
\ No newline at end of file
diff --git a/project/emo.html b/project/emo.html
new file mode 100644
index 00000000..3469cf0d
--- /dev/null
+++ b/project/emo.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ EM Optimization Lab | ngseke
+
+ EM Optimization Lab 《電磁最佳化實驗室》網站
這是台北科技大學之電磁最佳化實驗室 (EM Optimization Lab) 的網站,包含 RWD 來增進不同裝置上的瀏覽體驗。
前端主要以 Vue 架構,使用 Sass 和 Pug 搭配 Gulp 自動化流程工具編譯,使用 Firebase 作為資料庫。
後台管理 網站提供了簡單易用的後台介面,讓管理者能方便快速地更新頁面內容,如修改首頁近期活動 、上傳輪播照片 、 修改實驗室成員 等。
考慮到網站部署空間的限制,因為學校提供的上傳空間只支援靜態網頁,所以資料庫選擇使用 Firebase ,其最大的特色就是只靠靜態的前端網頁就可以操作 database。此外它也可以靠 JS 來上傳檔案到儲存空間,也提供驗證登入的功能,讓擁有權限的使用者才可進入後台編輯。
Demo https://myweb.ntut.edu.tw/~yschen/
+
+
+
+
\ No newline at end of file
diff --git a/project/flag.html b/project/flag.html
new file mode 100644
index 00000000..462ab680
--- /dev/null
+++ b/project/flag.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Raise Your Red Flag | ngseke
+
+ Raise Your Red Flag 以 Webcam 重現經典團康遊戲《紅旗舉起來》
+
+
+
+
\ No newline at end of file
diff --git a/project/gomoku.html b/project/gomoku.html
new file mode 100644
index 00000000..9e8fb537
--- /dev/null
+++ b/project/gomoku.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Gomoku | ngseke
+
+
+
+
+
+
\ No newline at end of file
diff --git a/project/iphone-price.html b/project/iphone-price.html
new file mode 100644
index 00000000..5b04d2af
--- /dev/null
+++ b/project/iphone-price.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ iPhone Price | ngseke
+
+ iPhone Price 台灣 iPhone 價格歷史趨勢
Vue TypeScript Tailwind daisyUI ECharts UI/UX
+
+
+
+
\ No newline at end of file
diff --git a/project/koasu.html b/project/koasu.html
new file mode 100644
index 00000000..c86390c1
--- /dev/null
+++ b/project/koasu.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Koasu | ngseke
+
+
+
+
+
+
\ No newline at end of file
diff --git a/project/leetcode-night.html b/project/leetcode-night.html
new file mode 100644
index 00000000..bf31fa29
--- /dev/null
+++ b/project/leetcode-night.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ LeetCode Night | ngseke
+
+ LeetCode Night LeetCode 深色模式瀏覽器擴充功能
React TypeScript i18n styled-components UI/UX
長久以來都只有中國版力扣 支援完整的暗黑模式,而美國 LeetCode 主站則僅有在 Problem List、Profile 等少數頁面頁才支援,獨缺解題頁。
某夜在深夜刷題的我突發奇想,說不定我可以取用力扣的色票,直接複寫 LeetCode 的樣式來實現深色模式,於是便誕生了這個專案。
原本也僅僅是寫好玩,就把它扔上了 GitHub,甚至連名字都是隨意取的。後來發現其實蠻多人 都在跟官方敲碗深色模式。因此後來我決定把它上架到商店,讓它變得更容易安裝。對我來說,這也是個難得可以從頭到尾,完整地開發擴充套件的機會。過程除了前端相關的開發、打包、上版號,甚至也包含 UI 和 Logo 設計 、截圖和宣傳橫幅的設計、發布送審套件等。
經過了半年左右時間,使用者人數終於突破了 1K,還獲得了藍勾 & 精選商品徽章,也算是達成了不值一提的小小成就。
簡介 LeetCode Night 是 Google Chrome 的擴充功能,讓使用者在題目頁也可以套用深色模式,即使在夜晚刷題也不必擔心瞎眼。
TypeScript UI 組件基於 Tocas UI v4 建構,搭配 styled-component 自定義樣式 使用 Axios 和 GraphQL 請求題目列表 使用 react-i18next 本地化 透過 Webpack css-loader 向頁面注入複寫用的 SASS 樣式
此外還有附加以下便利功能:
反相圖片顏色,讓題目中多數白底的插圖,在深色模式中也不至於太突兀 輸入 Question Number 來迅速跳轉至題目頁,此插件會將題目列表緩存至本機,無需等待 API 回應 自動清空編輯器裡前一次送出過的程式碼,特別適合刷題狂人,避免不小心瞄到答案
Demo
+
+
+
+
\ No newline at end of file
diff --git a/project/mcip-cms.html b/project/mcip-cms.html
new file mode 100644
index 00000000..976e59e5
--- /dev/null
+++ b/project/mcip-cms.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ MCIP CMS | ngseke
+
+
樂台計畫 誕生於 2018 年之冬,是我與大學好友鎧企(K7)攜手開發的大專院校音樂平台。我們皆來自吉他社,體認到各個賽事在籌辦時的痛點,於是樂台計畫應運而生。樂台計畫專為音樂賽事量身打造,宗旨是建構更優質的賽事環境,簡化社團在處理報名業務的作業流程。
服務上線至今,樂台計畫傾聽來自各方使用者的回饋和建議,持續優化系統與擴展規模。
對於參賽者,樂台計畫以 Line App 官方帳號的形式,讓使用者輕鬆簡單地報名參賽、瀏覽各賽事詳情,甚至可以查看當前比賽進度。目前使用者數已超過 6,000 人,全台從北到南,共計有 26 所大專院校的音樂社團成為我們的合作夥伴。
而對於主辦比賽的社團幹部們,樂台計畫提供一套完整的後台管理系統。在此專案中我主要負責的是後台管理系統 前端開發,與一部分 LINE App 的 UI/UX 。
管理後台大部分組件取自 Vuetify ,得益於這些開箱即用的 Material Design 風格組件,讓我們可以更專注在功能需求上,在初期得以快速地迭代原型。
此系統落實前後端分離,使用 Axios 與後端串接資料。我與後端合力規劃並協調 API 的規格,當中包含了 Database Schema 設計。在樂台計畫草創期,我們根據業務邏輯並考慮 NoSQL 的特性,妥善設計出合理的系統架構。
本專案使用 Pug 模板語言撰寫,透過縮排便能很靈活地調整 HTML 的巢狀結構。
MCIP Official Website 《樂台計畫》官方網站
+
+
+
+
\ No newline at end of file
diff --git a/project/mcip.html b/project/mcip.html
new file mode 100644
index 00000000..4814f90c
--- /dev/null
+++ b/project/mcip.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ MCIP Official Website | ngseke
+
+ MCIP Official Website 《樂台計畫》官方網站
樂台計畫官方網站 是以 Nuxt 建構的 SSR 形象網站。在此專案我負責的項目,包含定義網站架構與規格、UI/UX 設計、開發與部署。
關於《樂台計畫》平台詳細介紹請見MCIP CMS 。
緣由 樂台計畫在初期並未規劃官方網站,但隨著規模逐漸成長,有越來越多人與音樂社團對我們感到好奇,這讓我們重新思考品牌形象的建立與加強推廣。為了能接觸到更多群眾,讓更多人可以透過 Facebook 粉絲專頁以外的管道來認識我們,遂開始著手官方網站的構思。
舊版網站 在原本的前期版本,網站的定位只是一個陽春的入口網站,用來引導使用者加入樂台計畫 LINE App。因此當時是朝著單頁式網站的方向來設計,畫面上也只放置了 QRCode 等必要資訊。
為了盡快的製作出 prototype,當時選用 Parcel 這個易上手的打包工具。只要設定好進入點,它就會自動分析所有相依的資源,並自動封裝成 bundle,也省去很多繁複的手動配置。
以 Nuxt 重新架構 隨著網站的內容的不斷擴充,逐漸加入了「合作院校」、「聯絡我們」、「最新消息」和「FAQ」等區塊,我開始思考該如何優化網站 SEO,於是在後期決定採用 Nuxt 這個 SSR 解決方案,從頭開始打造新的網站。
在遷移到 Nuxt 後有許多好處,最顯而易見的就是預先抓取非同步資料,將內容渲染在頁面後才傳送到 client 端。其他像是 title、meta description 和 Open Graph 標籤等,也可以透過 Vue Meta 統一管理,一併預先渲染出來,有助搜尋引擎的爬蟲擷取頁面資料。
例如在社群媒體或是通訊軟體分享最新消息的文章時,縮圖和文章內容就能夠順利的被平台抓取,直接秀出預覽的資訊,提升使用者對連結的興趣。
部署 此專案部署在 Vercel 上,堪稱是最最無腦的託管平台。基本上只需要按照官方指南,在根目錄建立配置檔 vercel.json
,接著在平台上綁定 GitHub 版本庫即可。
在預設情況下 Vercel 就內建了 CI/CD,每當任何一個分支有新的 push,它都會立即執行建構和部署,並針對每個 commit 都產生出一個預覽用的 domain,方便驗證線上的結果。
Landing Page 的 Logo 動畫 我為識別 Logo 設計了簡單 SVG 動畫,藉著操控 stroke-dashoffset
和 stroke-dasharray
屬性,僅靠純 CSS3 就能達到酷炫的虛線描邊特效。
Demo https://mcip.app/
+
+
+
+
\ No newline at end of file
diff --git a/project/raise-your-red-flag.html b/project/raise-your-red-flag.html
new file mode 100644
index 00000000..462ab680
--- /dev/null
+++ b/project/raise-your-red-flag.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Raise Your Red Flag | ngseke
+
+ Raise Your Red Flag 以 Webcam 重現經典團康遊戲《紅旗舉起來》
+
+
+
+
\ No newline at end of file
diff --git a/project/shanlinliang.html b/project/shanlinliang.html
new file mode 100644
index 00000000..104a4967
--- /dev/null
+++ b/project/shanlinliang.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Shanlinliang | ngseke
+
+ Shanlinliang 虛構涼扇品牌廣告《扇林涼》
《扇林涼 Shanlinliang》 是一個虛構的涼扇品牌,運用簡約設計文字營造出商品的高級感。
+
+
+
+
\ No newline at end of file
diff --git a/project/tic-tac-toe.html b/project/tic-tac-toe.html
new file mode 100644
index 00000000..857d1e88
--- /dev/null
+++ b/project/tic-tac-toe.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Tic Tac Toe | ngseke
+
+
+
+
+
+
\ No newline at end of file
diff --git a/project/typing-typing.html b/project/typing-typing.html
new file mode 100644
index 00000000..751a31c8
--- /dev/null
+++ b/project/typing-typing.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Typing Typing! | ngseke
+
+ Typing Typing! 8-bit 復古風格打字遊戲
Feb, 2017 - Jun, 2017 Team Member: 余鎧企、 黃省喬
+
+
+
+
\ No newline at end of file
diff --git a/project/typingtyping.html b/project/typingtyping.html
new file mode 100644
index 00000000..f87093c1
--- /dev/null
+++ b/project/typingtyping.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Typing Typing! | ngseke
+
+ Typing Typing! 8-bit 復古風格打字遊戲
Feb, 2017 - Jun, 2017 Team Member: 余鎧企、 黃省喬
+
+
+
+
\ No newline at end of file
diff --git a/project/versatile-npm.html b/project/versatile-npm.html
new file mode 100644
index 00000000..417a5837
--- /dev/null
+++ b/project/versatile-npm.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Versatile Npm | ngseke
+
+ Versatile Npm 自訂 Npm 安裝指令瀏覽器擴充功能
簡介 Versatile Npm 是 Google Chrome 擴充功能,使用 Vue 3 + TypeScript + Chrome Extension API + Vuetify 建構。
這個擴充功能可以讓使用者在 npm 套件頁面的右側欄加入 yarn
和 pnpm
等安裝指令,方便開發者一鍵複製。
另外也提供設定 UI 供使用者自定義安裝指令,執行新增、刪除、編輯和排序操作。
Logo 這款 App 的 Logo 是透過 DALL·E 3 產生的,輸入 prompt 基本上包含 穿著帽 T 的水豚
、包裹
、紅色
、badge style
、sport mascot style
,最後透過 macOS 內建的「移除背景」功能去背。這過程幾乎不費吹灰之力,完全無需任何專業繪圖軟體。
其他候選 Logo 如下:
Demo
+
+
+
+
\ No newline at end of file
diff --git a/projects.html b/projects.html
new file mode 100644
index 00000000..43f74522
--- /dev/null
+++ b/projects.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Projects | ngseke
+
+ Browser Extension Taiwan Company Blocker 台灣求職網封鎖神器
Versatile Npm 自訂 Npm 安裝指令瀏覽器擴充功能
LeetCode Night LeetCode 深色模式瀏覽器擴充功能
Web iPhone Price 台灣 iPhone 價格歷史趨勢
MCIP Official Website 《樂台計畫》官方網站
Sinopac Dual Currency Card Calc. 永豐幣倍卡回饋計算機
EM Optimization Lab 《電磁最佳化實驗室》網站
System Design BOSS: Beverage Online Shop System 線上飲料購物系統
Game Raise Your Red Flag 以 Webcam 重現經典團康遊戲《紅旗舉起來》
Typing Typing! 8-bit 復古風格打字遊戲
Identity Design NTUT-CSIE & NTUB-ACC Camp, 2017 迎新《會炒不加辣,果資不加糖》品牌識別設計
Shanlinliang 虛構涼扇品牌廣告《扇林涼》
+
+
+
+
\ No newline at end of file
diff --git a/robots.txt b/robots.txt
new file mode 100644
index 00000000..fc1febf1
--- /dev/null
+++ b/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://ngseke.me/sitemap.xml
\ No newline at end of file
diff --git a/sitemap.xml b/sitemap.xml
new file mode 100644
index 00000000..658b745e
--- /dev/null
+++ b/sitemap.xml
@@ -0,0 +1 @@
+https://ngseke.me/about 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/blog 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/fe 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/ 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/projects 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/blog/bootstrap-in-nuxt 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/blog/check-if-key-exists 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/blog/code-review-taxi-number 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/blog/component-naming 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/blog/customize-youtube-caption-font 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/blog/react-handler-type 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/blog/reproduce-bootstrap-grid-in-tailwind 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/blog/typescript-as-const 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/blog/vite-vue-ts-eslint-setup 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/blog/webpack-config-esm 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/blog/wikipedia-link-converter 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/project/boss 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/project/camp-2017 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/project/credit-card-calc 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/project/em-optimization-lab 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/project/flag 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/project/gomoku 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/project/iphone-price 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/project/koasu 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/project/leetcode-night 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/project/mcip-cms 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/project/mcip 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/project/raise-your-red-flag 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/project/shanlinliang 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/project/tic-tac-toe 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/project/typing-typing 2023-11-29T15:46:31.680Z daily 1.0 https://ngseke.me/project/versatile-npm 2023-11-29T15:46:31.680Z daily 1.0
\ No newline at end of file
diff --git a/ssr-manifest.json b/ssr-manifest.json
new file mode 100644
index 00000000..a4a80efa
--- /dev/null
+++ b/ssr-manifest.json
@@ -0,0 +1,2633 @@
+{
+ "\u0000/home/runner/work/ngseke.me/ngseke.me/node_modules/.pnpm/dayjs@1.11.5/node_modules/dayjs/dayjs.min.js?commonjs-module": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "\u0000/home/runner/work/ngseke.me/ngseke.me/node_modules/.pnpm/nprogress@0.2.0/node_modules/nprogress/nprogress.js?commonjs-module": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "\u0000/home/runner/work/ngseke.me/ngseke.me/node_modules/.pnpm/performance-now@2.1.0/node_modules/performance-now/lib/performance-now.js?commonjs-module": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "\u0000/home/runner/work/ngseke.me/ngseke.me/node_modules/.pnpm/raf@3.4.1/node_modules/raf/index.js?commonjs-module": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "\u0000/home/runner/work/ngseke.me/ngseke.me/node_modules/.pnpm/scroll-to-element@2.0.3/node_modules/scroll-to-element/ease.js?commonjs-exports": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "\u0000/home/runner/work/ngseke.me/ngseke.me/node_modules/.pnpm/scroll-to-element@2.0.3/node_modules/scroll-to-element/emitter.js?commonjs-module": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "\u0000commonjsHelpers.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "\u0000plugin-vue:export-helper": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "\u0000vite/modulepreload-polyfill": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "\u0000vite/preload-helper": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "../../../../../@vite-plugin-pages/generated-pages?id=~pages": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "../../../../../@vite-plugin-pages/route-block": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "index.html": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/@fortawesome+fontawesome-svg-core@6.2.0/node_modules/@fortawesome/fontawesome-svg-core/index.mjs": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/@fortawesome+free-brands-svg-icons@6.2.0/node_modules/@fortawesome/free-brands-svg-icons/index.mjs": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/@fortawesome+free-solid-svg-icons@6.2.0/node_modules/@fortawesome/free-solid-svg-icons/index.mjs": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/@fortawesome+vue-fontawesome@3.0.1_@fortawesome+fontawesome-svg-core@6.2.0_vue@3.2.39/node_modules/@fortawesome/vue-fontawesome/index.es.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/@unhead+dom@1.7.4/node_modules/@unhead/dom/dist/index.mjs": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/@unhead+shared@1.7.4/node_modules/@unhead/shared/dist/index.mjs": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/@unhead+vue@1.7.4_vue@3.2.39/node_modules/@unhead/vue/dist/shared/vue.cf295fb1.mjs": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/@unhead+vue@1.7.4_vue@3.2.39/node_modules/@unhead/vue/dist/shared/vue.f36acd1f.mjs": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/@vue+reactivity@3.2.39/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/@vue+runtime-core@3.2.39/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/@vue+runtime-dom@3.2.39/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/@vue+shared@3.2.39/node_modules/@vue/shared/dist/shared.esm-bundler.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/@vueuse+core@9.13.0_vue@3.2.39/node_modules/@vueuse/core/index.mjs": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/@vueuse+integrations@9.13.0_axios@1.4.0_focus-trap@7.3.1_nprogress@0.2.0_vue@3.2.39/node_modules/@vueuse/integrations/useFocusTrap.mjs": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/@vueuse+shared@9.13.0_vue@3.2.39/node_modules/@vueuse/shared/index.mjs": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/dayjs@1.11.5/node_modules/dayjs/dayjs.min.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/focus-trap@7.3.1/node_modules/focus-trap/dist/focus-trap.esm.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/hookable@5.5.3/node_modules/hookable/dist/index.mjs": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/medium-zoom@1.0.6/node_modules/medium-zoom/dist/medium-zoom.esm.js": [
+ "/assets/useOgImage-203f60bd.js",
+ "/assets/useOgImage-588433c9.css"
+ ],
+ "node_modules/.pnpm/nanoid@3.3.6/node_modules/nanoid/index.browser.js": [
+ "/assets/useOgImage-203f60bd.js",
+ "/assets/useOgImage-588433c9.css"
+ ],
+ "node_modules/.pnpm/nanoid@4.0.2/node_modules/nanoid/index.browser.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/nprogress@0.2.0/node_modules/nprogress/nprogress.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/performance-now@2.1.0/node_modules/performance-now/lib/performance-now.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/raf@3.4.1/node_modules/raf/index.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/scroll-to-element@2.0.3/node_modules/scroll-to-element/ease.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/scroll-to-element@2.0.3/node_modules/scroll-to-element/emitter.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/scroll-to-element@2.0.3/node_modules/scroll-to-element/index.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/scroll-to-element@2.0.3/node_modules/scroll-to-element/scroll-to.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/scroll-to-element@2.0.3/node_modules/scroll-to-element/tween.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/tabbable@6.1.1/node_modules/tabbable/dist/index.esm.js": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/unhead@1.7.4/node_modules/unhead/dist/index.mjs": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/url-join@5.0.0/node_modules/url-join/lib/url-join.js": [
+ "/assets/useOgImage-203f60bd.js",
+ "/assets/useOgImage-588433c9.css"
+ ],
+ "node_modules/.pnpm/vite-ssg@0.23.3_critters@0.0.20_vite@4.4.11_vue-router@4.1.5_vue@3.2.39/node_modules/vite-ssg/dist/index.mjs": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/vite-ssg@0.23.3_critters@0.0.20_vite@4.4.11_vue-router@4.1.5_vue@3.2.39/node_modules/vite-ssg/dist/shared/vite-ssg.5912142e.mjs": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/vite-ssg@0.23.3_critters@0.0.20_vite@4.4.11_vue-router@4.1.5_vue@3.2.39/node_modules/vite-ssg/dist/shared/vite-ssg.a009fbf1.mjs": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/vue-router@4.1.5_vue@3.2.39/node_modules/vue-router/dist/vue-router.mjs": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "node_modules/.pnpm/vue-wrap-balancer@1.1.3_vue@3.2.39/node_modules/vue-wrap-balancer/dist/index.mjs": [
+ "/assets/useOgImage-203f60bd.js",
+ "/assets/useOgImage-588433c9.css"
+ ],
+ "src/App.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/assets/img/cover-background.jpg": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/assets/img/post/check-if-key-exists/valueOf.png": [
+ "/assets/check-if-key-exists-ce0584a7.js",
+ "/assets/valueOf-956b83dd.png"
+ ],
+ "src/assets/img/post/code-review-taxi-number/code-comments-be-like.jpg": [
+ "/assets/code-review-taxi-number-a7cd2111.js",
+ "/assets/code-comments-be-like-59cec7d1.jpg"
+ ],
+ "src/assets/img/post/component-naming/go-to-file.png": [
+ "/assets/component-naming-87122709.js",
+ "/assets/go-to-file-32915399.png",
+ "/assets/tabs-fc5ce7c7.png",
+ "/assets/open-editors-844d14f7.png"
+ ],
+ "src/assets/img/post/component-naming/open-editors.png": [
+ "/assets/component-naming-87122709.js",
+ "/assets/go-to-file-32915399.png",
+ "/assets/tabs-fc5ce7c7.png",
+ "/assets/open-editors-844d14f7.png"
+ ],
+ "src/assets/img/post/component-naming/tabs.png": [
+ "/assets/component-naming-87122709.js",
+ "/assets/go-to-file-32915399.png",
+ "/assets/tabs-fc5ce7c7.png",
+ "/assets/open-editors-844d14f7.png"
+ ],
+ "src/assets/img/post/customize-youtube-caption-font/after.jpg": [
+ "/assets/customize-youtube-caption-font-3e0540da.js",
+ "/assets/youtube-cc-menu-f1682830.png",
+ "/assets/before-485cea7e.jpg",
+ "/assets/after-8a80ee82.jpg"
+ ],
+ "src/assets/img/post/customize-youtube-caption-font/before.jpg": [
+ "/assets/customize-youtube-caption-font-3e0540da.js",
+ "/assets/youtube-cc-menu-f1682830.png",
+ "/assets/before-485cea7e.jpg",
+ "/assets/after-8a80ee82.jpg"
+ ],
+ "src/assets/img/post/customize-youtube-caption-font/youtube-cc-menu.png": [
+ "/assets/customize-youtube-caption-font-3e0540da.js",
+ "/assets/youtube-cc-menu-f1682830.png",
+ "/assets/before-485cea7e.jpg",
+ "/assets/after-8a80ee82.jpg"
+ ],
+ "src/assets/img/post/react-handler-type/hover-type.png": [
+ "/assets/react-handler-type-3a8ff22d.js",
+ "/assets/hover-type-d5f8cd06.png"
+ ],
+ "src/assets/img/post/typescript-as-const/intellisense.png": [
+ "/assets/typescript-as-const-fb61f8e3.js",
+ "/assets/intellisense-e7791f21.png",
+ "/assets/rename-symbol-ac29a4c0.png"
+ ],
+ "src/assets/img/post/typescript-as-const/rename-symbol.png": [
+ "/assets/typescript-as-const-fb61f8e3.js",
+ "/assets/intellisense-e7791f21.png",
+ "/assets/rename-symbol-ac29a4c0.png"
+ ],
+ "src/assets/img/post/vite-vue-ts-eslint-setup/command-reload-window.png": [
+ "/assets/vite-vue-ts-eslint-setup-1bf5e98d.js",
+ "/assets/output-parserOptions-project-error-ceeaa787.png",
+ "/assets/output-parserOptions-project-without-error-74ff031b.png",
+ "/assets/tsconfig-include-a4dd9125.png",
+ "/assets/command-reload-window-955490ff.png",
+ "/assets/vscode-linting-result-f315bd6a.png",
+ "/assets/vue-file-error-73be3796.png",
+ "/assets/move-object-properties-99f2f466.gif",
+ "/assets/vscode-auto-fix-on-save-445e3f9d.gif",
+ "/assets/terminal-lint-09df797c.png",
+ "/assets/eslint-automatically-fixable-4b63f152.png"
+ ],
+ "src/assets/img/post/vite-vue-ts-eslint-setup/eslint-automatically-fixable.png": [
+ "/assets/vite-vue-ts-eslint-setup-1bf5e98d.js",
+ "/assets/output-parserOptions-project-error-ceeaa787.png",
+ "/assets/output-parserOptions-project-without-error-74ff031b.png",
+ "/assets/tsconfig-include-a4dd9125.png",
+ "/assets/command-reload-window-955490ff.png",
+ "/assets/vscode-linting-result-f315bd6a.png",
+ "/assets/vue-file-error-73be3796.png",
+ "/assets/move-object-properties-99f2f466.gif",
+ "/assets/vscode-auto-fix-on-save-445e3f9d.gif",
+ "/assets/terminal-lint-09df797c.png",
+ "/assets/eslint-automatically-fixable-4b63f152.png"
+ ],
+ "src/assets/img/post/vite-vue-ts-eslint-setup/move-object-properties.gif": [
+ "/assets/vite-vue-ts-eslint-setup-1bf5e98d.js",
+ "/assets/output-parserOptions-project-error-ceeaa787.png",
+ "/assets/output-parserOptions-project-without-error-74ff031b.png",
+ "/assets/tsconfig-include-a4dd9125.png",
+ "/assets/command-reload-window-955490ff.png",
+ "/assets/vscode-linting-result-f315bd6a.png",
+ "/assets/vue-file-error-73be3796.png",
+ "/assets/move-object-properties-99f2f466.gif",
+ "/assets/vscode-auto-fix-on-save-445e3f9d.gif",
+ "/assets/terminal-lint-09df797c.png",
+ "/assets/eslint-automatically-fixable-4b63f152.png"
+ ],
+ "src/assets/img/post/vite-vue-ts-eslint-setup/output-parserOptions-project-error.png": [
+ "/assets/vite-vue-ts-eslint-setup-1bf5e98d.js",
+ "/assets/output-parserOptions-project-error-ceeaa787.png",
+ "/assets/output-parserOptions-project-without-error-74ff031b.png",
+ "/assets/tsconfig-include-a4dd9125.png",
+ "/assets/command-reload-window-955490ff.png",
+ "/assets/vscode-linting-result-f315bd6a.png",
+ "/assets/vue-file-error-73be3796.png",
+ "/assets/move-object-properties-99f2f466.gif",
+ "/assets/vscode-auto-fix-on-save-445e3f9d.gif",
+ "/assets/terminal-lint-09df797c.png",
+ "/assets/eslint-automatically-fixable-4b63f152.png"
+ ],
+ "src/assets/img/post/vite-vue-ts-eslint-setup/output-parserOptions-project-without-error.png": [
+ "/assets/vite-vue-ts-eslint-setup-1bf5e98d.js",
+ "/assets/output-parserOptions-project-error-ceeaa787.png",
+ "/assets/output-parserOptions-project-without-error-74ff031b.png",
+ "/assets/tsconfig-include-a4dd9125.png",
+ "/assets/command-reload-window-955490ff.png",
+ "/assets/vscode-linting-result-f315bd6a.png",
+ "/assets/vue-file-error-73be3796.png",
+ "/assets/move-object-properties-99f2f466.gif",
+ "/assets/vscode-auto-fix-on-save-445e3f9d.gif",
+ "/assets/terminal-lint-09df797c.png",
+ "/assets/eslint-automatically-fixable-4b63f152.png"
+ ],
+ "src/assets/img/post/vite-vue-ts-eslint-setup/terminal-lint.png": [
+ "/assets/vite-vue-ts-eslint-setup-1bf5e98d.js",
+ "/assets/output-parserOptions-project-error-ceeaa787.png",
+ "/assets/output-parserOptions-project-without-error-74ff031b.png",
+ "/assets/tsconfig-include-a4dd9125.png",
+ "/assets/command-reload-window-955490ff.png",
+ "/assets/vscode-linting-result-f315bd6a.png",
+ "/assets/vue-file-error-73be3796.png",
+ "/assets/move-object-properties-99f2f466.gif",
+ "/assets/vscode-auto-fix-on-save-445e3f9d.gif",
+ "/assets/terminal-lint-09df797c.png",
+ "/assets/eslint-automatically-fixable-4b63f152.png"
+ ],
+ "src/assets/img/post/vite-vue-ts-eslint-setup/tsconfig-include.png": [
+ "/assets/vite-vue-ts-eslint-setup-1bf5e98d.js",
+ "/assets/output-parserOptions-project-error-ceeaa787.png",
+ "/assets/output-parserOptions-project-without-error-74ff031b.png",
+ "/assets/tsconfig-include-a4dd9125.png",
+ "/assets/command-reload-window-955490ff.png",
+ "/assets/vscode-linting-result-f315bd6a.png",
+ "/assets/vue-file-error-73be3796.png",
+ "/assets/move-object-properties-99f2f466.gif",
+ "/assets/vscode-auto-fix-on-save-445e3f9d.gif",
+ "/assets/terminal-lint-09df797c.png",
+ "/assets/eslint-automatically-fixable-4b63f152.png"
+ ],
+ "src/assets/img/post/vite-vue-ts-eslint-setup/vscode-auto-fix-on-save.gif": [
+ "/assets/vite-vue-ts-eslint-setup-1bf5e98d.js",
+ "/assets/output-parserOptions-project-error-ceeaa787.png",
+ "/assets/output-parserOptions-project-without-error-74ff031b.png",
+ "/assets/tsconfig-include-a4dd9125.png",
+ "/assets/command-reload-window-955490ff.png",
+ "/assets/vscode-linting-result-f315bd6a.png",
+ "/assets/vue-file-error-73be3796.png",
+ "/assets/move-object-properties-99f2f466.gif",
+ "/assets/vscode-auto-fix-on-save-445e3f9d.gif",
+ "/assets/terminal-lint-09df797c.png",
+ "/assets/eslint-automatically-fixable-4b63f152.png"
+ ],
+ "src/assets/img/post/vite-vue-ts-eslint-setup/vscode-linting-result.png": [
+ "/assets/vite-vue-ts-eslint-setup-1bf5e98d.js",
+ "/assets/output-parserOptions-project-error-ceeaa787.png",
+ "/assets/output-parserOptions-project-without-error-74ff031b.png",
+ "/assets/tsconfig-include-a4dd9125.png",
+ "/assets/command-reload-window-955490ff.png",
+ "/assets/vscode-linting-result-f315bd6a.png",
+ "/assets/vue-file-error-73be3796.png",
+ "/assets/move-object-properties-99f2f466.gif",
+ "/assets/vscode-auto-fix-on-save-445e3f9d.gif",
+ "/assets/terminal-lint-09df797c.png",
+ "/assets/eslint-automatically-fixable-4b63f152.png"
+ ],
+ "src/assets/img/post/vite-vue-ts-eslint-setup/vue-file-error.png": [
+ "/assets/vite-vue-ts-eslint-setup-1bf5e98d.js",
+ "/assets/output-parserOptions-project-error-ceeaa787.png",
+ "/assets/output-parserOptions-project-without-error-74ff031b.png",
+ "/assets/tsconfig-include-a4dd9125.png",
+ "/assets/command-reload-window-955490ff.png",
+ "/assets/vscode-linting-result-f315bd6a.png",
+ "/assets/vue-file-error-73be3796.png",
+ "/assets/move-object-properties-99f2f466.gif",
+ "/assets/vscode-auto-fix-on-save-445e3f9d.gif",
+ "/assets/terminal-lint-09df797c.png",
+ "/assets/eslint-automatically-fixable-4b63f152.png"
+ ],
+ "src/assets/img/post/wikipedia-link-converter/after.png": [
+ "/assets/wikipedia-link-converter-5743973b.js",
+ "/assets/before-7a436972.png",
+ "/assets/after-97d65a70.png"
+ ],
+ "src/assets/img/post/wikipedia-link-converter/before.png": [
+ "/assets/wikipedia-link-converter-5743973b.js",
+ "/assets/before-7a436972.png",
+ "/assets/after-97d65a70.png"
+ ],
+ "src/assets/img/project/boss/boss1.png": [
+ "/assets/boss-7d1971fc.js",
+ "/assets/boss1-d440988b.png",
+ "/assets/boss2-3442c269.png",
+ "/assets/boss3-5ed82877.png",
+ "/assets/boss4-2c92bcf2.png",
+ "/assets/boss5-c63dc0c0.png"
+ ],
+ "src/assets/img/project/boss/boss2.png": [
+ "/assets/boss-7d1971fc.js",
+ "/assets/boss1-d440988b.png",
+ "/assets/boss2-3442c269.png",
+ "/assets/boss3-5ed82877.png",
+ "/assets/boss4-2c92bcf2.png",
+ "/assets/boss5-c63dc0c0.png"
+ ],
+ "src/assets/img/project/boss/boss3.png": [
+ "/assets/boss-7d1971fc.js",
+ "/assets/boss1-d440988b.png",
+ "/assets/boss2-3442c269.png",
+ "/assets/boss3-5ed82877.png",
+ "/assets/boss4-2c92bcf2.png",
+ "/assets/boss5-c63dc0c0.png"
+ ],
+ "src/assets/img/project/boss/boss4.png": [
+ "/assets/boss-7d1971fc.js",
+ "/assets/boss1-d440988b.png",
+ "/assets/boss2-3442c269.png",
+ "/assets/boss3-5ed82877.png",
+ "/assets/boss4-2c92bcf2.png",
+ "/assets/boss5-c63dc0c0.png"
+ ],
+ "src/assets/img/project/boss/boss5.png": [
+ "/assets/boss-7d1971fc.js",
+ "/assets/boss1-d440988b.png",
+ "/assets/boss2-3442c269.png",
+ "/assets/boss3-5ed82877.png",
+ "/assets/boss4-2c92bcf2.png",
+ "/assets/boss5-c63dc0c0.png"
+ ],
+ "src/assets/img/project/camp2017/bumu1.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/bumu2.jpg": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/dahezhao.jpg": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/datoutie1.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/datoutie2.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/datoutie3.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/datoutie4.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/datoutie_fb.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/fb_zhuanye.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/hengfu.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/icon.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/logo_en.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/logo_fb.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/mingpai1.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/mingpai2.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/shouce/0.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/shouce/1.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/shouce/2.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/shouce/3.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/shouce/4.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/shouce/5.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/shouce/6.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/shouce/7.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/shouce1.jpg": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/shouce2.jpg": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/tixing.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/tshirt1.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/camp2017/tshirt2.png": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/assets/img/project/credit-card-calc/cover.png": [
+ "/assets/credit-card-calc-28bb5562.js",
+ "/assets/cover-76f3ee6d.png"
+ ],
+ "src/assets/img/project/emo/cover.png": [
+ "/assets/em-optimization-lab-d89b3c86.js",
+ "/assets/cover-cf0ba68f.png",
+ "/assets/login-7406d8e1.png",
+ "/assets/manage-thesis-1289eceb.png",
+ "/assets/manage-research-b7b5b46f.png",
+ "/assets/manage-member-9cbfc70b.png",
+ "/assets/manage-carousel-5d0aae2a.png"
+ ],
+ "src/assets/img/project/emo/login.png": [
+ "/assets/em-optimization-lab-d89b3c86.js",
+ "/assets/cover-cf0ba68f.png",
+ "/assets/login-7406d8e1.png",
+ "/assets/manage-thesis-1289eceb.png",
+ "/assets/manage-research-b7b5b46f.png",
+ "/assets/manage-member-9cbfc70b.png",
+ "/assets/manage-carousel-5d0aae2a.png"
+ ],
+ "src/assets/img/project/emo/manage-carousel.png": [
+ "/assets/em-optimization-lab-d89b3c86.js",
+ "/assets/cover-cf0ba68f.png",
+ "/assets/login-7406d8e1.png",
+ "/assets/manage-thesis-1289eceb.png",
+ "/assets/manage-research-b7b5b46f.png",
+ "/assets/manage-member-9cbfc70b.png",
+ "/assets/manage-carousel-5d0aae2a.png"
+ ],
+ "src/assets/img/project/emo/manage-member.png": [
+ "/assets/em-optimization-lab-d89b3c86.js",
+ "/assets/cover-cf0ba68f.png",
+ "/assets/login-7406d8e1.png",
+ "/assets/manage-thesis-1289eceb.png",
+ "/assets/manage-research-b7b5b46f.png",
+ "/assets/manage-member-9cbfc70b.png",
+ "/assets/manage-carousel-5d0aae2a.png"
+ ],
+ "src/assets/img/project/emo/manage-research.png": [
+ "/assets/em-optimization-lab-d89b3c86.js",
+ "/assets/cover-cf0ba68f.png",
+ "/assets/login-7406d8e1.png",
+ "/assets/manage-thesis-1289eceb.png",
+ "/assets/manage-research-b7b5b46f.png",
+ "/assets/manage-member-9cbfc70b.png",
+ "/assets/manage-carousel-5d0aae2a.png"
+ ],
+ "src/assets/img/project/emo/manage-thesis.png": [
+ "/assets/em-optimization-lab-d89b3c86.js",
+ "/assets/cover-cf0ba68f.png",
+ "/assets/login-7406d8e1.png",
+ "/assets/manage-thesis-1289eceb.png",
+ "/assets/manage-research-b7b5b46f.png",
+ "/assets/manage-member-9cbfc70b.png",
+ "/assets/manage-carousel-5d0aae2a.png"
+ ],
+ "src/assets/img/project/flag/cover.png": [
+ "/assets/raise-your-red-flag-6df7832f.js",
+ "/assets/cover-c3bb2266.png"
+ ],
+ "src/assets/img/project/gomoku/cover.png": [
+ "/assets/gomoku-e312c04b.js",
+ "/assets/register-0126b216.png",
+ "/assets/room-f8da92ed.png",
+ "/assets/cover-bdee11bd.png",
+ "/assets/mobile-bc89f109.png"
+ ],
+ "src/assets/img/project/gomoku/mobile.png": [
+ "/assets/gomoku-e312c04b.js",
+ "/assets/register-0126b216.png",
+ "/assets/room-f8da92ed.png",
+ "/assets/cover-bdee11bd.png",
+ "/assets/mobile-bc89f109.png"
+ ],
+ "src/assets/img/project/gomoku/register.png": [
+ "/assets/gomoku-e312c04b.js",
+ "/assets/register-0126b216.png",
+ "/assets/room-f8da92ed.png",
+ "/assets/cover-bdee11bd.png",
+ "/assets/mobile-bc89f109.png"
+ ],
+ "src/assets/img/project/gomoku/room.png": [
+ "/assets/gomoku-e312c04b.js",
+ "/assets/register-0126b216.png",
+ "/assets/room-f8da92ed.png",
+ "/assets/cover-bdee11bd.png",
+ "/assets/mobile-bc89f109.png"
+ ],
+ "src/assets/img/project/gomoku/title.png": [
+ "/assets/gomoku-e312c04b.js",
+ "/assets/register-0126b216.png",
+ "/assets/room-f8da92ed.png",
+ "/assets/cover-bdee11bd.png",
+ "/assets/mobile-bc89f109.png"
+ ],
+ "src/assets/img/project/iphone-price/1.png": [
+ "/assets/iphone-price-c267c7d7.js",
+ "/assets/cover-37fabbf0.png",
+ "/assets/1-408d5807.png",
+ "/assets/2-c1925776.png",
+ "/assets/3-c180c5f6.png"
+ ],
+ "src/assets/img/project/iphone-price/2.png": [
+ "/assets/iphone-price-c267c7d7.js",
+ "/assets/cover-37fabbf0.png",
+ "/assets/1-408d5807.png",
+ "/assets/2-c1925776.png",
+ "/assets/3-c180c5f6.png"
+ ],
+ "src/assets/img/project/iphone-price/3.png": [
+ "/assets/iphone-price-c267c7d7.js",
+ "/assets/cover-37fabbf0.png",
+ "/assets/1-408d5807.png",
+ "/assets/2-c1925776.png",
+ "/assets/3-c180c5f6.png"
+ ],
+ "src/assets/img/project/iphone-price/cover.png": [
+ "/assets/iphone-price-c267c7d7.js",
+ "/assets/cover-37fabbf0.png",
+ "/assets/1-408d5807.png",
+ "/assets/2-c1925776.png",
+ "/assets/3-c180c5f6.png"
+ ],
+ "src/assets/img/project/koasu/cover.png": [
+ "/assets/koasu-5245e6bf.js",
+ "/assets/cover-c99f79a0.png"
+ ],
+ "src/assets/img/project/leetcode-night/1.png": [
+ "/assets/leetcode-night-b75823ff.js",
+ "/assets/banner-63e3ec60.png",
+ "/assets/store-f420ca97.png",
+ "/assets/2-8417bfde.png",
+ "/assets/1-d295ad8e.png"
+ ],
+ "src/assets/img/project/leetcode-night/2.png": [
+ "/assets/leetcode-night-b75823ff.js",
+ "/assets/banner-63e3ec60.png",
+ "/assets/store-f420ca97.png",
+ "/assets/2-8417bfde.png",
+ "/assets/1-d295ad8e.png"
+ ],
+ "src/assets/img/project/leetcode-night/banner.png": [
+ "/assets/leetcode-night-b75823ff.js",
+ "/assets/banner-63e3ec60.png",
+ "/assets/store-f420ca97.png",
+ "/assets/2-8417bfde.png",
+ "/assets/1-d295ad8e.png"
+ ],
+ "src/assets/img/project/leetcode-night/store.png": [
+ "/assets/leetcode-night-b75823ff.js",
+ "/assets/banner-63e3ec60.png",
+ "/assets/store-f420ca97.png",
+ "/assets/2-8417bfde.png",
+ "/assets/1-d295ad8e.png"
+ ],
+ "src/assets/img/project/mcip-cms/competition.png": [
+ "/assets/mcip-cms-dcfd5ebb.js",
+ "/assets/fb-cover-6fb81625.png",
+ "/assets/dashboard-cc7d8fff.png",
+ "/assets/forms-d0d210e4.png",
+ "/assets/edit-form-5c88d403.png",
+ "/assets/login-3ac1d3a4.png",
+ "/assets/pug-67d261e0.png",
+ "/assets/competition-1f600c5f.png",
+ "/assets/config-21adacbc.png"
+ ],
+ "src/assets/img/project/mcip-cms/config.png": [
+ "/assets/mcip-cms-dcfd5ebb.js",
+ "/assets/fb-cover-6fb81625.png",
+ "/assets/dashboard-cc7d8fff.png",
+ "/assets/forms-d0d210e4.png",
+ "/assets/edit-form-5c88d403.png",
+ "/assets/login-3ac1d3a4.png",
+ "/assets/pug-67d261e0.png",
+ "/assets/competition-1f600c5f.png",
+ "/assets/config-21adacbc.png"
+ ],
+ "src/assets/img/project/mcip-cms/dashboard.png": [
+ "/assets/mcip-cms-dcfd5ebb.js",
+ "/assets/fb-cover-6fb81625.png",
+ "/assets/dashboard-cc7d8fff.png",
+ "/assets/forms-d0d210e4.png",
+ "/assets/edit-form-5c88d403.png",
+ "/assets/login-3ac1d3a4.png",
+ "/assets/pug-67d261e0.png",
+ "/assets/competition-1f600c5f.png",
+ "/assets/config-21adacbc.png"
+ ],
+ "src/assets/img/project/mcip-cms/edit-form.png": [
+ "/assets/mcip-cms-dcfd5ebb.js",
+ "/assets/fb-cover-6fb81625.png",
+ "/assets/dashboard-cc7d8fff.png",
+ "/assets/forms-d0d210e4.png",
+ "/assets/edit-form-5c88d403.png",
+ "/assets/login-3ac1d3a4.png",
+ "/assets/pug-67d261e0.png",
+ "/assets/competition-1f600c5f.png",
+ "/assets/config-21adacbc.png"
+ ],
+ "src/assets/img/project/mcip-cms/fb-cover.png": [
+ "/assets/mcip-cms-dcfd5ebb.js",
+ "/assets/fb-cover-6fb81625.png",
+ "/assets/dashboard-cc7d8fff.png",
+ "/assets/forms-d0d210e4.png",
+ "/assets/edit-form-5c88d403.png",
+ "/assets/login-3ac1d3a4.png",
+ "/assets/pug-67d261e0.png",
+ "/assets/competition-1f600c5f.png",
+ "/assets/config-21adacbc.png"
+ ],
+ "src/assets/img/project/mcip-cms/forms.png": [
+ "/assets/mcip-cms-dcfd5ebb.js",
+ "/assets/fb-cover-6fb81625.png",
+ "/assets/dashboard-cc7d8fff.png",
+ "/assets/forms-d0d210e4.png",
+ "/assets/edit-form-5c88d403.png",
+ "/assets/login-3ac1d3a4.png",
+ "/assets/pug-67d261e0.png",
+ "/assets/competition-1f600c5f.png",
+ "/assets/config-21adacbc.png"
+ ],
+ "src/assets/img/project/mcip-cms/login.png": [
+ "/assets/mcip-cms-dcfd5ebb.js",
+ "/assets/fb-cover-6fb81625.png",
+ "/assets/dashboard-cc7d8fff.png",
+ "/assets/forms-d0d210e4.png",
+ "/assets/edit-form-5c88d403.png",
+ "/assets/login-3ac1d3a4.png",
+ "/assets/pug-67d261e0.png",
+ "/assets/competition-1f600c5f.png",
+ "/assets/config-21adacbc.png"
+ ],
+ "src/assets/img/project/mcip-cms/pug.png": [
+ "/assets/mcip-cms-dcfd5ebb.js",
+ "/assets/fb-cover-6fb81625.png",
+ "/assets/dashboard-cc7d8fff.png",
+ "/assets/forms-d0d210e4.png",
+ "/assets/edit-form-5c88d403.png",
+ "/assets/login-3ac1d3a4.png",
+ "/assets/pug-67d261e0.png",
+ "/assets/competition-1f600c5f.png",
+ "/assets/config-21adacbc.png"
+ ],
+ "src/assets/img/project/mcip/1.png": [
+ "/assets/mcip-28b770e5.js",
+ "/assets/1-c36f7e53.png",
+ "/assets/2-4f25068f.png",
+ "/assets/3-ff7def49.png",
+ "/assets/legacy-42002bd1.png",
+ "/assets/facebook-messenger-bf65dd89.png",
+ "/assets/deployment-f152146e.png",
+ "/assets/logo-animation-3865b365.gif"
+ ],
+ "src/assets/img/project/mcip/2.png": [
+ "/assets/mcip-28b770e5.js",
+ "/assets/1-c36f7e53.png",
+ "/assets/2-4f25068f.png",
+ "/assets/3-ff7def49.png",
+ "/assets/legacy-42002bd1.png",
+ "/assets/facebook-messenger-bf65dd89.png",
+ "/assets/deployment-f152146e.png",
+ "/assets/logo-animation-3865b365.gif"
+ ],
+ "src/assets/img/project/mcip/3.png": [
+ "/assets/mcip-28b770e5.js",
+ "/assets/1-c36f7e53.png",
+ "/assets/2-4f25068f.png",
+ "/assets/3-ff7def49.png",
+ "/assets/legacy-42002bd1.png",
+ "/assets/facebook-messenger-bf65dd89.png",
+ "/assets/deployment-f152146e.png",
+ "/assets/logo-animation-3865b365.gif"
+ ],
+ "src/assets/img/project/mcip/deployment.png": [
+ "/assets/mcip-28b770e5.js",
+ "/assets/1-c36f7e53.png",
+ "/assets/2-4f25068f.png",
+ "/assets/3-ff7def49.png",
+ "/assets/legacy-42002bd1.png",
+ "/assets/facebook-messenger-bf65dd89.png",
+ "/assets/deployment-f152146e.png",
+ "/assets/logo-animation-3865b365.gif"
+ ],
+ "src/assets/img/project/mcip/facebook-messenger.png": [
+ "/assets/mcip-28b770e5.js",
+ "/assets/1-c36f7e53.png",
+ "/assets/2-4f25068f.png",
+ "/assets/3-ff7def49.png",
+ "/assets/legacy-42002bd1.png",
+ "/assets/facebook-messenger-bf65dd89.png",
+ "/assets/deployment-f152146e.png",
+ "/assets/logo-animation-3865b365.gif"
+ ],
+ "src/assets/img/project/mcip/legacy.png": [
+ "/assets/mcip-28b770e5.js",
+ "/assets/1-c36f7e53.png",
+ "/assets/2-4f25068f.png",
+ "/assets/3-ff7def49.png",
+ "/assets/legacy-42002bd1.png",
+ "/assets/facebook-messenger-bf65dd89.png",
+ "/assets/deployment-f152146e.png",
+ "/assets/logo-animation-3865b365.gif"
+ ],
+ "src/assets/img/project/mcip/logo-animation.gif": [
+ "/assets/mcip-28b770e5.js",
+ "/assets/1-c36f7e53.png",
+ "/assets/2-4f25068f.png",
+ "/assets/3-ff7def49.png",
+ "/assets/legacy-42002bd1.png",
+ "/assets/facebook-messenger-bf65dd89.png",
+ "/assets/deployment-f152146e.png",
+ "/assets/logo-animation-3865b365.gif"
+ ],
+ "src/assets/img/project/shanlinliang/cover.png": [
+ "/assets/shanlinliang-9edbff22.js",
+ "/assets/cover-ca6a18cd.png",
+ "/assets/sll1-98c64e6a.png",
+ "/assets/sll4-b982d4df.png",
+ "/assets/sll5-a9b2ee83.png"
+ ],
+ "src/assets/img/project/shanlinliang/sll1.png": [
+ "/assets/shanlinliang-9edbff22.js",
+ "/assets/cover-ca6a18cd.png",
+ "/assets/sll1-98c64e6a.png",
+ "/assets/sll4-b982d4df.png",
+ "/assets/sll5-a9b2ee83.png"
+ ],
+ "src/assets/img/project/shanlinliang/sll4.png": [
+ "/assets/shanlinliang-9edbff22.js",
+ "/assets/cover-ca6a18cd.png",
+ "/assets/sll1-98c64e6a.png",
+ "/assets/sll4-b982d4df.png",
+ "/assets/sll5-a9b2ee83.png"
+ ],
+ "src/assets/img/project/shanlinliang/sll5.png": [
+ "/assets/shanlinliang-9edbff22.js",
+ "/assets/cover-ca6a18cd.png",
+ "/assets/sll1-98c64e6a.png",
+ "/assets/sll4-b982d4df.png",
+ "/assets/sll5-a9b2ee83.png"
+ ],
+ "src/assets/img/project/tic-tac-toe/1.png": [
+ "/assets/tic-tac-toe-dd64aee5.js",
+ "/assets/1-ce441fa9.png",
+ "/assets/2-6e0e5062.png"
+ ],
+ "src/assets/img/project/tic-tac-toe/2.png": [
+ "/assets/tic-tac-toe-dd64aee5.js",
+ "/assets/1-ce441fa9.png",
+ "/assets/2-6e0e5062.png"
+ ],
+ "src/assets/img/project/typingtyping/char1.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/char2.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/char3.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/enemy1.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/enemy2.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/enemy3.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/game1.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/game2.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/game3.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/game4.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/game5.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/instruction1.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/instruction2.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/instruction3.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/instruction4.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/instruction5.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/instruction6.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/instruction7.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/interface1.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/interface2.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/interface3.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/interface4.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/interface5.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/typingtyping/menu.png": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/assets/img/project/versatile-npm/1400x560.png": [
+ "/assets/versatile-npm-d3f5c477.js",
+ "/assets/1400x560-02b6cc6e.png",
+ "/assets/demo-222ae85f.gif",
+ "/assets/screenshot-2-9726f1a7.png",
+ "/assets/screenshot-1-81b8fb35.png",
+ "/assets/logo-candidates-4f73c7bd.png"
+ ],
+ "src/assets/img/project/versatile-npm/demo.gif": [
+ "/assets/versatile-npm-d3f5c477.js",
+ "/assets/1400x560-02b6cc6e.png",
+ "/assets/demo-222ae85f.gif",
+ "/assets/screenshot-2-9726f1a7.png",
+ "/assets/screenshot-1-81b8fb35.png",
+ "/assets/logo-candidates-4f73c7bd.png"
+ ],
+ "src/assets/img/project/versatile-npm/logo-candidates.png": [
+ "/assets/versatile-npm-d3f5c477.js",
+ "/assets/1400x560-02b6cc6e.png",
+ "/assets/demo-222ae85f.gif",
+ "/assets/screenshot-2-9726f1a7.png",
+ "/assets/screenshot-1-81b8fb35.png",
+ "/assets/logo-candidates-4f73c7bd.png"
+ ],
+ "src/assets/img/project/versatile-npm/screenshot-1.png": [
+ "/assets/versatile-npm-d3f5c477.js",
+ "/assets/1400x560-02b6cc6e.png",
+ "/assets/demo-222ae85f.gif",
+ "/assets/screenshot-2-9726f1a7.png",
+ "/assets/screenshot-1-81b8fb35.png",
+ "/assets/logo-candidates-4f73c7bd.png"
+ ],
+ "src/assets/img/project/versatile-npm/screenshot-2.png": [
+ "/assets/versatile-npm-d3f5c477.js",
+ "/assets/1400x560-02b6cc6e.png",
+ "/assets/demo-222ae85f.gif",
+ "/assets/screenshot-2-9726f1a7.png",
+ "/assets/screenshot-1-81b8fb35.png",
+ "/assets/logo-candidates-4f73c7bd.png"
+ ],
+ "src/components/AboutMe.vue": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/AboutMeSectionContactMe.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/AboutMeSectionExperience.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/AboutMeSectionLayout.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/AboutMeSectionPosts.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/AboutMeSectionProjects.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/AboutMeSectionSelfIntroduction.vue": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/AboutMeSectionSkills.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/AlertReadAllProject.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/projects-34cb3ecc.js"
+ ],
+ "src/components/Button.vue": [
+ "/assets/projects-34cb3ecc.js"
+ ],
+ "src/components/ButtonDarkMode.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/ButtonHamburger.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/ButtonRotateIcon.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/CardProject.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/CardProjectImageGlow.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/CardProjectLinkOverlay.vue": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/Cover.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/CoverBody.vue": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/CoverBody.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/CoverBody.vue?vue&type=style&index=0&scoped=b1cdf682&lang.scss": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/Emphasis.vue": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/IsomorphicLink.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/Link.vue": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/LinkIcon.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/LinkIconDemo.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/LinkIconGithub.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/LinkPost.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/LinkViewMore.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/NavbarHamburger.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/NavbarLink.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/NavbarLogo.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/OgImageTemplate.vue": [
+ "/assets/useOgImage-203f60bd.js",
+ "/assets/useOgImage-588433c9.css"
+ ],
+ "src/components/OgImageTemplate.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/useOgImage-203f60bd.js",
+ "/assets/useOgImage-588433c9.css"
+ ],
+ "src/components/OgImageTemplate.vue?vue&type=style&index=0&scoped=7d4dce4c&lang.scss": [
+ "/assets/useOgImage-203f60bd.js",
+ "/assets/useOgImage-588433c9.css"
+ ],
+ "src/components/PostLayout.vue": [
+ "/assets/useOgImage-203f60bd.js",
+ "/assets/useOgImage-588433c9.css"
+ ],
+ "src/components/PostList.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/PostTags.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/blog-f75762ab.js"
+ ],
+ "src/components/ProjectList.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/ProjectTags.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/useOgImage-203f60bd.js",
+ "/assets/useOgImage-588433c9.css"
+ ],
+ "src/components/Prose.vue": [
+ "/assets/useOgImage-203f60bd.js",
+ "/assets/useOgImage-588433c9.css"
+ ],
+ "src/components/Prose.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/useOgImage-203f60bd.js",
+ "/assets/useOgImage-588433c9.css"
+ ],
+ "src/components/Prose.vue?vue&type=style&index=0&scoped=eb8991d3&lang.scss": [
+ "/assets/useOgImage-203f60bd.js",
+ "/assets/useOgImage-588433c9.css"
+ ],
+ "src/components/TeamMembers.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/project-e24afe95.js"
+ ],
+ "src/components/TheFooter.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/TheNavbar.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/components/TitleCategory.vue": [
+ "/assets/projects-34cb3ecc.js"
+ ],
+ "src/components/TransitionUniversal.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/composables/useDark.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/composables/useDownplayProjects.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/composables/useHamburger.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/composables/useIsCoverVisible.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/composables/useIsScrolledToTop.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/composables/useMediumZoom.ts": [
+ "/assets/useOgImage-203f60bd.js",
+ "/assets/useOgImage-588433c9.css"
+ ],
+ "src/composables/useMountedBodyClass.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/composables/useNProgress.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/composables/useOgImage.ts": [
+ "/assets/useOgImage-203f60bd.js",
+ "/assets/useOgImage-588433c9.css"
+ ],
+ "src/composables/usePostFrontmatter.ts": [
+ "/assets/blog-f75762ab.js"
+ ],
+ "src/composables/usePosts.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/composables/useProjectFrontmatter.ts": [
+ "/assets/project-e24afe95.js"
+ ],
+ "src/composables/useProjects.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/composables/useReadHistory.ts": [
+ "/assets/useReadHistory-d98c4b5d.js"
+ ],
+ "src/composables/useRootViewTransition.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/composables/useScrollToAnchor.ts": [
+ "/assets/useOgImage-203f60bd.js",
+ "/assets/useOgImage-588433c9.css"
+ ],
+ "src/index.sass": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/main.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/modules/about-me-section.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/modules/constants.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/modules/date.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/modules/links.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/pages/404.vue": [
+ "/assets/404-c7f247d5.js"
+ ],
+ "src/pages/[...404].vue": [
+ "/assets/_...404_-c7f247d5.js"
+ ],
+ "src/pages/about.vue": [
+ "/assets/about-c7f247d5.js"
+ ],
+ "src/pages/blog.vue": [
+ "/assets/blog-f75762ab.js"
+ ],
+ "src/pages/blog.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/blog-f75762ab.js"
+ ],
+ "src/pages/blog/bootstrap-in-nuxt.md": [
+ "/assets/bootstrap-in-nuxt-1bb6f56c.js"
+ ],
+ "src/pages/blog/bootstrap-in-nuxt.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/bootstrap-in-nuxt-1bb6f56c.js"
+ ],
+ "src/pages/blog/check-if-key-exists.md": [
+ "/assets/check-if-key-exists-ce0584a7.js",
+ "/assets/valueOf-956b83dd.png"
+ ],
+ "src/pages/blog/check-if-key-exists.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/check-if-key-exists-ce0584a7.js",
+ "/assets/valueOf-956b83dd.png"
+ ],
+ "src/pages/blog/code-review-taxi-number.md": [
+ "/assets/code-review-taxi-number-a7cd2111.js",
+ "/assets/code-comments-be-like-59cec7d1.jpg"
+ ],
+ "src/pages/blog/code-review-taxi-number.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/code-review-taxi-number-a7cd2111.js",
+ "/assets/code-comments-be-like-59cec7d1.jpg"
+ ],
+ "src/pages/blog/component-naming.md": [
+ "/assets/component-naming-87122709.js",
+ "/assets/go-to-file-32915399.png",
+ "/assets/tabs-fc5ce7c7.png",
+ "/assets/open-editors-844d14f7.png"
+ ],
+ "src/pages/blog/component-naming.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/component-naming-87122709.js",
+ "/assets/go-to-file-32915399.png",
+ "/assets/tabs-fc5ce7c7.png",
+ "/assets/open-editors-844d14f7.png"
+ ],
+ "src/pages/blog/customize-youtube-caption-font.md": [
+ "/assets/customize-youtube-caption-font-3e0540da.js",
+ "/assets/youtube-cc-menu-f1682830.png",
+ "/assets/before-485cea7e.jpg",
+ "/assets/after-8a80ee82.jpg"
+ ],
+ "src/pages/blog/customize-youtube-caption-font.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/customize-youtube-caption-font-3e0540da.js",
+ "/assets/youtube-cc-menu-f1682830.png",
+ "/assets/before-485cea7e.jpg",
+ "/assets/after-8a80ee82.jpg"
+ ],
+ "src/pages/blog/index.vue": [
+ "/assets/index-4cd242c9.js"
+ ],
+ "src/pages/blog/index.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/index-4cd242c9.js"
+ ],
+ "src/pages/blog/react-handler-type.md": [
+ "/assets/react-handler-type-3a8ff22d.js",
+ "/assets/hover-type-d5f8cd06.png"
+ ],
+ "src/pages/blog/react-handler-type.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/react-handler-type-3a8ff22d.js",
+ "/assets/hover-type-d5f8cd06.png"
+ ],
+ "src/pages/blog/reproduce-bootstrap-grid-in-tailwind.md": [
+ "/assets/reproduce-bootstrap-grid-in-tailwind-ce693ef5.js"
+ ],
+ "src/pages/blog/reproduce-bootstrap-grid-in-tailwind.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/reproduce-bootstrap-grid-in-tailwind-ce693ef5.js"
+ ],
+ "src/pages/blog/typescript-as-const.md": [
+ "/assets/typescript-as-const-fb61f8e3.js",
+ "/assets/intellisense-e7791f21.png",
+ "/assets/rename-symbol-ac29a4c0.png"
+ ],
+ "src/pages/blog/typescript-as-const.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/typescript-as-const-fb61f8e3.js",
+ "/assets/intellisense-e7791f21.png",
+ "/assets/rename-symbol-ac29a4c0.png"
+ ],
+ "src/pages/blog/vite-vue-ts-eslint-setup.md": [
+ "/assets/vite-vue-ts-eslint-setup-1bf5e98d.js",
+ "/assets/output-parserOptions-project-error-ceeaa787.png",
+ "/assets/output-parserOptions-project-without-error-74ff031b.png",
+ "/assets/tsconfig-include-a4dd9125.png",
+ "/assets/command-reload-window-955490ff.png",
+ "/assets/vscode-linting-result-f315bd6a.png",
+ "/assets/vue-file-error-73be3796.png",
+ "/assets/move-object-properties-99f2f466.gif",
+ "/assets/vscode-auto-fix-on-save-445e3f9d.gif",
+ "/assets/terminal-lint-09df797c.png",
+ "/assets/eslint-automatically-fixable-4b63f152.png"
+ ],
+ "src/pages/blog/vite-vue-ts-eslint-setup.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/vite-vue-ts-eslint-setup-1bf5e98d.js",
+ "/assets/output-parserOptions-project-error-ceeaa787.png",
+ "/assets/output-parserOptions-project-without-error-74ff031b.png",
+ "/assets/tsconfig-include-a4dd9125.png",
+ "/assets/command-reload-window-955490ff.png",
+ "/assets/vscode-linting-result-f315bd6a.png",
+ "/assets/vue-file-error-73be3796.png",
+ "/assets/move-object-properties-99f2f466.gif",
+ "/assets/vscode-auto-fix-on-save-445e3f9d.gif",
+ "/assets/terminal-lint-09df797c.png",
+ "/assets/eslint-automatically-fixable-4b63f152.png"
+ ],
+ "src/pages/blog/webpack-config-esm.md": [
+ "/assets/webpack-config-esm-320be40c.js"
+ ],
+ "src/pages/blog/webpack-config-esm.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/webpack-config-esm-320be40c.js"
+ ],
+ "src/pages/blog/wikipedia-link-converter.md": [
+ "/assets/wikipedia-link-converter-5743973b.js",
+ "/assets/before-7a436972.png",
+ "/assets/after-97d65a70.png"
+ ],
+ "src/pages/blog/wikipedia-link-converter.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/wikipedia-link-converter-5743973b.js",
+ "/assets/before-7a436972.png",
+ "/assets/after-97d65a70.png"
+ ],
+ "src/pages/fe.vue": [
+ "/assets/fe-e736b23f.js"
+ ],
+ "src/pages/fe.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/fe-e736b23f.js"
+ ],
+ "src/pages/index.vue": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/pages/index.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/cover-background-f6446981.jpg"
+ ],
+ "src/pages/project.vue": [
+ "/assets/project-e24afe95.js"
+ ],
+ "src/pages/project.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/project-e24afe95.js"
+ ],
+ "src/pages/project/boss.md": [
+ "/assets/boss-7d1971fc.js",
+ "/assets/boss1-d440988b.png",
+ "/assets/boss2-3442c269.png",
+ "/assets/boss3-5ed82877.png",
+ "/assets/boss4-2c92bcf2.png",
+ "/assets/boss5-c63dc0c0.png"
+ ],
+ "src/pages/project/boss.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/boss-7d1971fc.js",
+ "/assets/boss1-d440988b.png",
+ "/assets/boss2-3442c269.png",
+ "/assets/boss3-5ed82877.png",
+ "/assets/boss4-2c92bcf2.png",
+ "/assets/boss5-c63dc0c0.png"
+ ],
+ "src/pages/project/camp-2017.md": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/pages/project/camp-2017.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/camp-2017-d632b91c.js",
+ "/assets/logo_fb-3a5fbf8a.png",
+ "/assets/icon-3f390287.png",
+ "/assets/bumu1-2902b5f3.png",
+ "/assets/logo_en-8478642b.png",
+ "/assets/bumu2-9fbc6c06.jpg",
+ "/assets/mingpai1-50edfb95.png",
+ "/assets/mingpai2-dead019a.png",
+ "/assets/tshirt1-4f8fb94c.png",
+ "/assets/tshirt2-c61c550c.png",
+ "/assets/hengfu-324e5744.png",
+ "/assets/fb_zhuanye-8a3106ac.png",
+ "/assets/tixing-f836dc82.png",
+ "/assets/dahezhao-4534134f.jpg",
+ "/assets/datoutie1-f730def3.png",
+ "/assets/datoutie2-b626c0ae.png",
+ "/assets/datoutie3-99933bc0.png",
+ "/assets/datoutie4-8d7f399d.png",
+ "/assets/datoutie_fb-b37840ad.png",
+ "/assets/0-11090461.png",
+ "/assets/1-f5f11eb3.png",
+ "/assets/2-a067597c.png",
+ "/assets/3-78ab6d35.png",
+ "/assets/4-708e3caa.png",
+ "/assets/5-0224294b.png",
+ "/assets/6-a159bc1d.png",
+ "/assets/7-cb414675.png",
+ "/assets/shouce1-193ed0a8.jpg",
+ "/assets/shouce2-0a3f097e.jpg"
+ ],
+ "src/pages/project/camp2017.vue": [
+ "/assets/camp2017-8a21aa39.js"
+ ],
+ "src/pages/project/credit-card-calc.md": [
+ "/assets/credit-card-calc-28bb5562.js",
+ "/assets/cover-76f3ee6d.png"
+ ],
+ "src/pages/project/credit-card-calc.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/credit-card-calc-28bb5562.js",
+ "/assets/cover-76f3ee6d.png"
+ ],
+ "src/pages/project/em-optimization-lab.md": [
+ "/assets/em-optimization-lab-d89b3c86.js",
+ "/assets/cover-cf0ba68f.png",
+ "/assets/login-7406d8e1.png",
+ "/assets/manage-thesis-1289eceb.png",
+ "/assets/manage-research-b7b5b46f.png",
+ "/assets/manage-member-9cbfc70b.png",
+ "/assets/manage-carousel-5d0aae2a.png"
+ ],
+ "src/pages/project/em-optimization-lab.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/em-optimization-lab-d89b3c86.js",
+ "/assets/cover-cf0ba68f.png",
+ "/assets/login-7406d8e1.png",
+ "/assets/manage-thesis-1289eceb.png",
+ "/assets/manage-research-b7b5b46f.png",
+ "/assets/manage-member-9cbfc70b.png",
+ "/assets/manage-carousel-5d0aae2a.png"
+ ],
+ "src/pages/project/emo.vue": [
+ "/assets/emo-5557c288.js"
+ ],
+ "src/pages/project/flag.vue": [
+ "/assets/flag-5557c288.js"
+ ],
+ "src/pages/project/gomoku.md": [
+ "/assets/gomoku-e312c04b.js",
+ "/assets/register-0126b216.png",
+ "/assets/room-f8da92ed.png",
+ "/assets/cover-bdee11bd.png",
+ "/assets/mobile-bc89f109.png"
+ ],
+ "src/pages/project/gomoku.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/gomoku-e312c04b.js",
+ "/assets/register-0126b216.png",
+ "/assets/room-f8da92ed.png",
+ "/assets/cover-bdee11bd.png",
+ "/assets/mobile-bc89f109.png"
+ ],
+ "src/pages/project/index.vue": [
+ "/assets/index-95e78139.js"
+ ],
+ "src/pages/project/iphone-price.md": [
+ "/assets/iphone-price-c267c7d7.js",
+ "/assets/cover-37fabbf0.png",
+ "/assets/1-408d5807.png",
+ "/assets/2-c1925776.png",
+ "/assets/3-c180c5f6.png"
+ ],
+ "src/pages/project/iphone-price.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/iphone-price-c267c7d7.js",
+ "/assets/cover-37fabbf0.png",
+ "/assets/1-408d5807.png",
+ "/assets/2-c1925776.png",
+ "/assets/3-c180c5f6.png"
+ ],
+ "src/pages/project/koasu.md": [
+ "/assets/koasu-5245e6bf.js",
+ "/assets/cover-c99f79a0.png"
+ ],
+ "src/pages/project/koasu.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/koasu-5245e6bf.js",
+ "/assets/cover-c99f79a0.png"
+ ],
+ "src/pages/project/leetcode-night.md": [
+ "/assets/leetcode-night-b75823ff.js",
+ "/assets/banner-63e3ec60.png",
+ "/assets/store-f420ca97.png",
+ "/assets/2-8417bfde.png",
+ "/assets/1-d295ad8e.png"
+ ],
+ "src/pages/project/leetcode-night.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/leetcode-night-b75823ff.js",
+ "/assets/banner-63e3ec60.png",
+ "/assets/store-f420ca97.png",
+ "/assets/2-8417bfde.png",
+ "/assets/1-d295ad8e.png"
+ ],
+ "src/pages/project/mcip-cms.md": [
+ "/assets/mcip-cms-dcfd5ebb.js",
+ "/assets/fb-cover-6fb81625.png",
+ "/assets/dashboard-cc7d8fff.png",
+ "/assets/forms-d0d210e4.png",
+ "/assets/edit-form-5c88d403.png",
+ "/assets/login-3ac1d3a4.png",
+ "/assets/pug-67d261e0.png",
+ "/assets/competition-1f600c5f.png",
+ "/assets/config-21adacbc.png"
+ ],
+ "src/pages/project/mcip-cms.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/mcip-cms-dcfd5ebb.js",
+ "/assets/fb-cover-6fb81625.png",
+ "/assets/dashboard-cc7d8fff.png",
+ "/assets/forms-d0d210e4.png",
+ "/assets/edit-form-5c88d403.png",
+ "/assets/login-3ac1d3a4.png",
+ "/assets/pug-67d261e0.png",
+ "/assets/competition-1f600c5f.png",
+ "/assets/config-21adacbc.png"
+ ],
+ "src/pages/project/mcip.md": [
+ "/assets/mcip-28b770e5.js",
+ "/assets/1-c36f7e53.png",
+ "/assets/2-4f25068f.png",
+ "/assets/3-ff7def49.png",
+ "/assets/legacy-42002bd1.png",
+ "/assets/facebook-messenger-bf65dd89.png",
+ "/assets/deployment-f152146e.png",
+ "/assets/logo-animation-3865b365.gif"
+ ],
+ "src/pages/project/mcip.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/mcip-28b770e5.js",
+ "/assets/1-c36f7e53.png",
+ "/assets/2-4f25068f.png",
+ "/assets/3-ff7def49.png",
+ "/assets/legacy-42002bd1.png",
+ "/assets/facebook-messenger-bf65dd89.png",
+ "/assets/deployment-f152146e.png",
+ "/assets/logo-animation-3865b365.gif"
+ ],
+ "src/pages/project/raise-your-red-flag.md": [
+ "/assets/raise-your-red-flag-6df7832f.js",
+ "/assets/cover-c3bb2266.png"
+ ],
+ "src/pages/project/raise-your-red-flag.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/raise-your-red-flag-6df7832f.js",
+ "/assets/cover-c3bb2266.png"
+ ],
+ "src/pages/project/shanlinliang.md": [
+ "/assets/shanlinliang-9edbff22.js",
+ "/assets/cover-ca6a18cd.png",
+ "/assets/sll1-98c64e6a.png",
+ "/assets/sll4-b982d4df.png",
+ "/assets/sll5-a9b2ee83.png"
+ ],
+ "src/pages/project/shanlinliang.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/shanlinliang-9edbff22.js",
+ "/assets/cover-ca6a18cd.png",
+ "/assets/sll1-98c64e6a.png",
+ "/assets/sll4-b982d4df.png",
+ "/assets/sll5-a9b2ee83.png"
+ ],
+ "src/pages/project/tic-tac-toe.md": [
+ "/assets/tic-tac-toe-dd64aee5.js",
+ "/assets/1-ce441fa9.png",
+ "/assets/2-6e0e5062.png"
+ ],
+ "src/pages/project/tic-tac-toe.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/tic-tac-toe-dd64aee5.js",
+ "/assets/1-ce441fa9.png",
+ "/assets/2-6e0e5062.png"
+ ],
+ "src/pages/project/typing-typing.md": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/pages/project/typing-typing.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/typing-typing-876fd270.js",
+ "/assets/menu-d3272083.png",
+ "/assets/char2-9248d6c0.png",
+ "/assets/char3-5ba16f0d.png",
+ "/assets/game1-d689a75e.png",
+ "/assets/game2-ec9b0a6e.png",
+ "/assets/game3-ae563b09.png",
+ "/assets/game4-ad0db896.png",
+ "/assets/interface3-2a7d9ac4.png",
+ "/assets/interface4-6491aa95.png",
+ "/assets/interface5-fd8930fa.png"
+ ],
+ "src/pages/project/typingtyping.vue": [
+ "/assets/typingtyping-b132256f.js"
+ ],
+ "src/pages/project/versatile-npm.md": [
+ "/assets/versatile-npm-d3f5c477.js",
+ "/assets/1400x560-02b6cc6e.png",
+ "/assets/demo-222ae85f.gif",
+ "/assets/screenshot-2-9726f1a7.png",
+ "/assets/screenshot-1-81b8fb35.png",
+ "/assets/logo-candidates-4f73c7bd.png"
+ ],
+ "src/pages/project/versatile-npm.md?vue&type=script&setup=true&lang.ts": [
+ "/assets/versatile-npm-d3f5c477.js",
+ "/assets/1400x560-02b6cc6e.png",
+ "/assets/demo-222ae85f.gif",
+ "/assets/screenshot-2-9726f1a7.png",
+ "/assets/screenshot-1-81b8fb35.png",
+ "/assets/logo-candidates-4f73c7bd.png"
+ ],
+ "src/pages/projects.vue": [
+ "/assets/projects-34cb3ecc.js"
+ ],
+ "src/pages/projects.vue?vue&type=script&setup=true&lang.ts": [
+ "/assets/projects-34cb3ecc.js"
+ ]
+}
\ No newline at end of file