我的Vue之旅 06 超詳細、仿 itch.io 主頁設計(Mobile)

来源:https://www.cnblogs.com/linxiaoxu/archive/2022/10/18/16804597.html
-Advertisement-
Play Games

第二期 · 使用 Vue 3.1 + TypeScript + Router + Tailwind.css 仿 itch.io 平臺主頁。 我的主題 HapiGames 是仿 itch.io 的 indie game hosting marketplace。 效果圖 代碼倉庫 alicepolice ...


第二期 · 使用 Vue 3.1 + TypeScript + Router + Tailwind.css 仿 itch.io 平臺主頁。

我的主題 HapiGames 是仿 itch.ioindie game hosting marketplace

效果圖

image-20221018221613004


代碼倉庫

alicepolice/Vue at 06 (github.com)

風格指南

當你掌握一門語言的時候,在寫項目之前不妨先看看風格指南吧,前人早為你鋪好了路。下麵是我自己編寫項目代碼時沒有規範到位的幾個點。

風格指南 — Vue.js (vuejs.org)


Prop 定義

Prop 定義應該儘量詳細,至少需要指定其類型。Props | Vue.js (vuejs.org)

Vue的選項式API為我們提供了Prop校驗,你可以向 props 選項提供一個帶有 props 校驗選項的對象,當 prop 的校驗失敗後,Vue 會拋出一個控制台警告 (開發模式)。(如果用ts的話更好)

註意 prop 的校驗是在組件實例被創建之前,所以實例的屬性 (比如 datacomputed 等) 將在 defaultvalidator 函數中不可用。


v-for和v-if同時在一個標簽時,將v-if提取到計算屬性

因為 v-for 優先順序比 v-if 高,所以每次渲染時必定會遍曆數組所有元素。避免 v-if 和 v-for 用在一起

將v-if提取到計算屬性後的好處

  • 過濾後的列表會在對應數組發生相關變化時才被重新運算,過濾更高效。
  • 使用 v-for="item in afterComputed" 之後,在渲染的時候遍歷元素少了,渲染更高效。
  • 解耦渲染層的邏輯,可維護性 (對邏輯的更改和擴展) 更強。

緊密耦合的組件名

和父組件緊密耦合的子組件應該以父組件名作為首碼命名。緊密耦合的組件名

如果一個組件只在某個父組件的場景下有意義,這層關係應該體現在其名字上。因為編輯器通常會按字母順序組織文件,所以這樣做可以把相關聯的文件排在一起。

不建議為了緊密耦合搞目錄區分,因為會出現文件名名字相同、IDE側邊欄瀏覽組件花費時間多的問題。

components/
|- TodoList.vue
|- TodoListItem.vue
|- TodoListItemButton.vue

自閉合組件

在單文件組件、字元串模板和 JSX 中沒有內容的組件應該是自閉合的——但在 DOM 模板里永遠不要這樣做。 自閉合組件

<!-- 在單文件組件、字元串模板和 JSX 中 -->
<MyComponent/>

<!-- 在 DOM 模板中 -->
<my-component></my-component>

Prop 名大小寫

在聲明 prop 的時候,其命名應該始終使用 camelCase,而在模板和 JSX 中應該始終使用 kebab-case。 Prop 名大小寫

props: {
  greetingText: String
}
<WelcomeMessage greeting-text="hi"/>

簡單的計算屬性

應該把複雜計算屬性分割為儘可能多的更簡單的 property。 簡單的計算屬性

好處是易於測試、易於閱讀、更好的“擁抱變化”。


單文件組件的頂級元素的順序

單文件組件應該總是讓 <script>、<template> 和 <style> 標簽的順序保持一致。且 <style> 要放在最後,因為另外兩個標簽至少要有一個。 單文件組件的頂級元素的順序


隱性的父子組件通信

應該優先通過 prop 和事件進行父子組件之間的通信,而不是 this.$parent 或變更 prop。 隱性的父子組件通信

數據流應該是單向的,不要反向修改 props。


方便調試

為了方便調試,我們在 index.css 下新增一個樣式組合,通過添加test類樣式類看到塊元素的邊框。

  .test{
    @apply border border-gray-900
  }

目錄結構

├───assets
│   ├───avater
│   │   用戶頭像
│   ├───blog
│   │   博文封面圖
│   ├───diffuse
│   │   模糊背景
│   ├───game
│   │   游戲封面圖
│   ├───logo
│   │   網站logo
│   ├───slideshow
│   │	輪播圖樣圖
│   └───svg
│		很多矢量圖
├───components
│   ├───common
│   │       BottomBar.vue
│   │       CommentArea.vue
│   │       SideBar.vue
│   │       SideBarHref.vue
│   │       SlideShow.vue
│   │       TopBar.vue
│   │
│   └───HomeView
│           GameBlog.vue
│           GameInfo.vue
│           GameList.vue
│           HomeFAQ.vue
│           HomeFooter.vue
│           PlatformNavigation.vue
│           TopNavigation.vue
│
├───router
│       index.ts
└───views
        AboutView.vue
        CommentTestView.vue
        HomeLoginView.vue
        HomeView.vue
        LoginView.vue
        RegisterView.vue

網站頂部組件 TopBar.vue

在 src/components/common 下新建 TopBar.vue,並移入之前寫的 BottomBar.vue。

先從網站頂部開始,該組件在每個頁面都會顯示,併在滾動過程中固定定位。

編寫代碼,實現頂部欄。

image-20221015182929055

<template>
  <div class="h-12 shadow-md">
    <div class="inline-block h-full w-16">
      <b-icon-list class="text-3xl mt-2 ml-4"></b-icon-list>
    </div>
    <div class="inline-block h-full w-48">
      <img src="@/assets/logo/logo3.png" class="mt-1 h-4/5 w-full" />
    </div>
    <div class="inline-block float-right mt-2.5 mr-4">
      <div
        class="
          border-2 border-gray-300
          px-3.5
          py-0.5
          rounded-sm
          text-sm
          font-bold
        "
      >
        Log in
      </div>
    </div>
  </div>
</template>

側邊導航欄組件 SideBar.vue

image-20221018221323986

註釋底部導航欄

我的Vue之旅、05 導航欄、登錄、註冊 (Mobile) - 小能日記 - 博客園 (cnblogs.com)

在前一期內容中,我們創建的導航欄是底部導航欄

image-20220928215915019

現在我們推倒重來,實現一下側邊導航欄

側邊欄導航也叫抽屜式導航是隱藏在界面側邊的位置,一般是通過點擊界面左上角的icon彈出,主要承載的內容是除了核心功能意外的主要功能。側邊欄還分全側邊和半側邊。

img

當我們在App.vue中註釋掉現有的底部導航欄,此時會出現錯誤item.routerName => item對象的類型為 "unknown"。

<template>
  <router-view @set-bottom-flag="setBottomFlag" />
  <!-- <BottomBar v-show="bottomFlag" :items="bottomItems" /> -->
</template>


<script lang="ts">
import { defineComponent } from "vue";
// import BottomBar from "@/components/BottomBar.vue";

export default defineComponent({
  name: "App",
  components: {
    // BottomBar,
  }
  ...

[TS]使用高級類型PropType註釋props類型

IDE報錯並不影響當前Vue實例,因為BottomBar組件並未掛載。但為了去除報錯,使用高級類型註釋來修改BottomBar.vue。

運行時 props 選項僅支持使用構造函數來作為一個 prop 的類型,沒有辦法指定多層級對象或函數簽名之類的複雜類型。在這裡可以使用 PropType 註釋複雜的props類型,報錯解決。

<script lang="ts">
import { PropType } from "vue";

interface BottomItem {
  text: string;
  icon: string;
  routerName: string;
}
export default {
  props: {
    items: {
      type: Array as PropType<BottomItem[]>,
      required: true,
    },
  },
};
</script>

SideBarHref.vue

在 src/components/common 下新建 SideBarHref.vue

側邊導航欄有相似之處,不妨將這一塊提取成獨立的組件,然後復用三次。

image-20221018214833334

添加樣式 hover:text-rose-500 hover:underline,在移動端按下時會改變顏色。

image-20221018215234879

<a :href="value.href">用於臨時超鏈接占位,後續可改為router-link

<template>
  <div class="mt-8 mx-2">
    <div class="font-bold text-stone-700 text-sm">{{ items.title }}</div>
    <div class="w-full text-stone-600 mt-2 text-sm">
      <template v-for="(value, index) in items.items" :key="index">
        <a :href="value.href">
          <div
            class="
              py-1
              inline-block
              w-1/2
              align-middle
              hover:text-rose-500 hover:underline
            "
            v-html="value.text"
          ></div>
        </a>
      </template>
    </div>
  </div>
</template>

<script lang="ts">
import { PropType } from "vue";

interface item {
  text: string;
  href: string;
}

interface items {
  items: item[];
  title: string;
}

export default {
  name: "SideBarHref",
  props: {
    items: { type: Object as PropType<items>, required: true },
  },
};
</script>

SideBar.vue

遮蔽層

在 src/components/common 下新建 SideBar.vue 以下代碼片段均為分段表示,不是完整代碼。

先寫model層(遮蔽層),一般指側邊欄滾出後背景變黑的部分。

image-20221018215658616

我們使用自定義類名實現過渡動畫。類名也是TailWind.css的類樣式,給定200毫秒時間,過渡透明度狀態。

div嵌套了兩層,把opacity-50寫到裡面的div層能解決opacity-50在外面div層的時候出現背景全黑問題。

fixed 用於固定遮蔽層。z-30用於設置優先順序,先顯示在前面。v-show由App.vue傳入,頂部組件通知App.vue事件對應的方法修改,進而引發當前transition的過渡。

html - Vue Transition with Tailwind - Stack Overflow

<template>
  <transition
    enter-active-class="duration-200"
    enter-from-class="opacity-0"
    enter-to-class="opacity-100"
    leave-active-class="duration-200"
    leave-to-class="opacity-0"
    leave-from-class="opacity-100"
  >
    <div class="fixed z-30 h-full w-full" v-show="showFlag" id="model">
      <div class="bg-black h-full w-full opacity-50"></div>
    </div>
  </transition>

側邊欄

側邊欄的動畫效果跟遮蔽層一個原理,只不過修改成為了移動而不是改變透明度。

overflow-auto 可以讓側邊欄在內容溢出時具備滾動條。

 <transition
    enter-active-class="duration-200 ease-out"
    enter-from-class="-translate-x-64"
    enter-to-class="translate-x-0"
    leave-active-class="duration-200 ease-in"
    leave-from-class="translate-x-0"
    leave-to-class="-translate-x-64"
  >
   <div
      class="fixed z-40 top-12 w-64 h-full bg-stone-100 border-r overflow-auto"
      v-show="showFlag"
      id="sideBar"
    >

搜索框

focus:outline-none focus:ring focus:border-blue-200噹噹前游標指向該input標簽時更改樣式,讓四角發光變藍。該段代碼也可以提取成基本組件。

image-20221018220623191

      <div class="mt-3 mx-2">
        <input
          id="search"
          class="
            bg-white
            focus:outline-none focus:ring focus:border-blue-200
            py-1.5
            pl-3
            w-full
            border border-gray-300
            text-sm
          "
          type="text"
          placeholder="Search games & creators"
          v-model="search"
        />
      </div>

SideBarHref

三次復用之前定義的SideBarHref組件,並傳入了props

      <SideBarHref :items="popularTags"></SideBarHref>
      <SideBarHref :items="browse"></SideBarHref>
      <SideBarHref :items="gamesByPrice"></SideBarHref>

download app

image-20221018220958319

讓圖標和下載超鏈接完全數據化,增加網頁動態變化能力。

      <div class="h-20 text-center">
        <div class="pt-6">
          <template v-for="(value, index) in appInfo.apps" :key="index">
            <a :href="value.href">
              <component
                :is="value.icon"
                class="inline m-1 text-xl hover:text-rose-500"
              ></component>
            </a>
          </template>
          <a :href="appInfo.download.href">
            <span
              class="
                text-xs text-stone-800
                mx-2
                hover:text-rose-500 hover:underline
              "
              >{{ appInfo.download.title }}</span
            >
          </a>
        </div>
      </div>

數據驅動

除非結構要改,現在完全可以靠data里的對象數據驅動當前側邊欄的所有內容。

  data() {
    return {
      search: "",
      popularTags: {
        title: "POPULAR TAGS",
        items: [
          { text: "Horror games", href: "" },
          { text: "Multiplayer", href: "" },
          { text: "Visual novels", href: "" },
          { text: "HTML5 games", href: "" },
          { text: "Simulation", href: "" },
          { text: "macOS games", href: "" },
          { text: "Roguelike", href: "" },
          { text: "Linux games", href: "" },
          { text: "Browse all tags", href: "" },
        ],
      },
      browse: {
        title: "BROWSE",
	  ....

聯動頂部組件與側邊導航欄組件

我們的想法是按下頂部組件左邊的 list icon,彈出導航欄,再按一次關閉導航欄。

image-20221018212545068

很容易想到父子通信的解決方案,這也是Vue單向數據流的最佳實現。

image-20221018213127850
<template>
  <TopBar @changeSideFlag="changeSideFlag"></TopBar>
  <SideBar :show-flag="sideFlag"></SideBar>
  <div class="absolute top-12 w-full z-10">
    <router-view />
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import TopBar from "./components/common/TopBar.vue";
import SideBar from "./components/common/SideBar.vue";

export default defineComponent({
  name: "App",
  components: {
    TopBar,
    SideBar,
  },
  data() {
    return {
      sideFlag: false as boolean,
    };
  },
  methods: {
    changeSideFlag(): void {
      this.sideFlag = !this.sideFlag;
    },
  },
});
</script>

[TS]defineComponent 作用

App.vue 里的 export default defineComponent({ 是什麼?

搭配 TypeScript 使用 Vue | Vue.js (vuejs.org)

defineComponent 是TypeScript獨有的,可以根據選項式API的props、data自動推導各個欄位的類型,當在生命周期函數、Methods函數、模板表達式中使用這些欄位時可以進行類型檢查。(不顯式引入編譯器預設自動引入)


移動端主頁

HomeView.vue

我們將一個主頁拆分為各個組件,並完全依托數據驅動,圖片僅用來本地測試。

<template>
  <HomeFAQ />
  <TopNavigation :top-navigation="topNavigation"></TopNavigation>
  <GameInfo :game-info="gameInfo"></GameInfo>
  <GameBlog :game-blog="gameBlog"></GameBlog>
  <PlatformNavigation
    :platform-navigation="platformNavigation"
  ></PlatformNavigation>
  <GameList :game-list="latestGames"></GameList>
  <GameList :game-list="mostFeatureGames"></GameList>
  <HomeFooter />
</template>

<script lang="ts">
import { defineComponent } from "vue";
import GameInfo from "../components/HomeView/GameInfo.vue";
import GameBlog from "../components/HomeView/GameBlog.vue";
import HomeFAQ from "../components/HomeView/HomeFAQ.vue";
import TopNavigation from "../components/HomeView/TopNavigation.vue";
import GameList from "../components/HomeView/GameList.vue";
import PlatformNavigation from "../components/HomeView/PlatformNavigation.vue";
import HomeFooter from "../components/HomeView/HomeFooter.vue";

export default defineComponent({
  name: "HomeView",
  components: {
    GameInfo,
    GameBlog,
    HomeFAQ,
    TopNavigation,
    GameList,
    PlatformNavigation,
    HomeFooter,
  },
  data() {
    return {
      topNavigation: [
        { text: "All Games", href: "" },
        { text: "Game jams", href: "" },
        { text: "Developer Logs", href: "" },
        { text: "Community", href: "" },
        { text: "Bundles", href: "" },
      ],
      gameInfo: {
        youtube:
          "https://www.youtube.com/embed/U7MJljsoUSo?autoplay=0&fs=0&iv_load_policy=3&showinfo=0&rel=0&cc_load_policy=0&start=0&end=0",
        title: "Baba Is You",
        desc: "You can change the rules by which you play",
        price: "$14.99",
        platforms: ["b-icon-windows", "b-icon-apple"],
        images: [
          require("@/assets/slideshow/1.png"),
          require("@/assets/slideshow/2.png"),
          require("@/assets/slideshow/3.png"),
          require("@/assets/slideshow/4.png"),
          require("@/assets/slideshow/5.png"),
        ],
      },
      gameBlog: [
        {
          title: "Games of the Month: surrealist solitaire puzzles",
          text: `What’s that? You need more games? I hear you, anonymous hapi fan.
          We’ve reached the part of the year when games start coming out fast`,
          img: require("@/assets/blog/1.jpg"),
        },
        {
          title: "Games of the Month: Puzzles!",
          text: `Sometimes you need a good puzzle game, just something to throw all of
          your attention at and ignore anything else going on. Well if that
          sometime for you is right now, then you’re in luck because in this
          Games of the Month`,
          img: require("@/assets/blog/2.jpg"),
        },
        {
          title: "The next hapi Creator Day is July 29th!",
          text: ` I don’t think I’m allowed to make the entire body of this post “The
          next itch.io Creator Day is taking place on Friday July 29th.” I mean
          it’s true, we are hosting the next itch.io Creator Day on Friday July
          29th but I should probably write more here.`,
          img: require("@/assets/blog/3.jpg"),
        },
      ],
      platformNavigation: [
        {
          title: "Windows",
          href: "",
          img: require("@/assets/svg/windows.svg"),
        },
        {
          title: "macOS",
          href: "",
          img: require("@/assets/svg/apple.svg"),
        },
        {
          title: "Linux",
          href: "",
          img: require("@/assets/svg/linux.svg"),
        },
        {
          title: "Android",
          href: "",
          img: require("@/assets/svg/android.svg"),
        },
        {
          title: "iOS",
          href: "",
          img: require("@/assets/svg/apple.svg"),
        },
        {
          title: "Web",
          href: "",
          img: require("@/assets/svg/web.svg"),
        },
        {
          title: "Free",
          href: "",
          img: require("@/assets/svg/free.svg"),
        },
        {
          title: "On Sale",
          href: "",
          img: require("@/assets/svg/sale.svg"),
        },
        {
          title: "Top Seller",
          href: "",
          img: require("@/assets/svg/star.svg"),
        },
        {
          title: "Recent",
          href: "",
          img: require("@/assets/svg/recent.svg"),
        },
      ],
      latestGames: {
        title: "Latest Featured Games",
        button: {
          title: "View all",
          href: "",
        },
        games: [
          {
            title: "Late Night Mop",
            text: "A haunted house cleaning simulator.",
            img: require("@/assets/game/1.png"),
            price: 0,
          },
          {
            title: "an average day at the cat cafe",
            text: "A haunted house cleaning simulator.",
            img: require("@/assets/game/2.png"),
            price: 0,
            web: true,
          },
          {
            title: "Corebreaker",
            text: "A fast-paced action-platform shooter game with roguelike elements.",
            img: require("@/assets/game/3.png"),
            price: 19.99,
            tags: ["Difficult", "Fast-Paced"],
          },
          {
            title: "Beacon Pines",
            text: "Normal isn't what it used to be.",
            img: require("@/assets/game/4.png"),
            price: 4.99,
          },
          {
            title: "Atuel",
            text: "Traverse a surrealist landscape inspired by the Atuel River in Argentina.",
            img: require("@/assets/game/5.png"),
            price: 0,
          },
        ],
      },
      mostFeatureGames: {
        title: "Most Featured Games",
        button: {
          title: "View all",
          href: "",
        },
        games: [
          {
            title: "Hitobito no Hikari - Heian Jidai",
            text: "A survival horror TTRPG about cursed priestesses.",
            img: require("@/assets/game/6.png"),
            tags: ["Physical games"],
            price: 3,
          },
          {
            title: "Doko Roko",
            text: "A symbiosis with ancient shadows. A tower full of demons. A proverb.",
            img: require("@/assets/game/7.png"),
            price: 10,
          },
          {
            title: "The Zachtronics Solitaire Collection",
            text: "All seven Zachtronics solitaire games, updated with new 4K graphics, plus one brand new Tarot-themed solitaire variant.",
            img: require("@/assets/game/8.png"),
            price: 9.99,
            tags: ["Card Game", "Singleplayer"],
          },
          {
            title: "Mixolumia",
            text: "Entrancing musical falling block puzzler.",
            img: require("@/assets/game/9.png"),
            price: 10,
            tags: ["High Score", "Arcade"],
          },
          {
            title: "Atuel",
            text: "Traverse a surrealist landscape inspired by the Atuel River in Argentina.",
            img: require("@/assets/game/5.png"),
            price: 0,
          },
          {
            title: "Corebreaker",
            text: "A fast-paced action-platform shooter game with roguelike elements.",
            img: require("@/assets/game/3.png"),
            price: 19.99,
            tags: ["Difficult", "Fast-Paced"],
          },
        ],
      },
    };
  },
});
</script>

HomeFAQ.vue

image-20221018222202189

<template>
  <div class="h-24 p-2 text-sm bg-stone-100">
    <div class="mt-1">
      <b>HapiGames</b> is a simple way to find and share indie games online for
      free.
    </div>
    <div class="mt-2">
      <a class="underline text-rose-500">Add your game</a> or
      <a class="underline text-rose-500">Read the FAQ</a>
    </div>
  </div>
</template>
<script>
export default {
  name: "HomeFAQ",
}
</script>

TopNavigation

image-20221018222616793

overflow-x-auto flex flex佈局,併在溢出時開啟橫軸滾動條

whitespace-nowrap 類可以防止換行,讓所有元素保持在一行上。

html - Div with horizontal scrolling only - Stack Overflow

<template>
  <div class="overflow-x-auto flex bg-white">
    <template v-for="(value, index) in topNavigation" :key="index">
      <a :href="value.href">
        <div
          class="
            p-3
            font-bold
            text-sm text-stone-800
            hover:text-rose-500
            whitespace-nowrap
          "
        >
          {{ value.text }}
        </div>
      </a>
    </template>
  </div>
</template>
<script lang="ts">
import { PropType } from "vue";

interface TopNavigation {
  text: string;
  href: string;
}

export default {
  name: "topNavigation",
  props: {
    topNavigation: {
      type: Array as PropType<TopNavigation[]>,
      required: true,
    },
  },
};
</script>

GameInfo.vue

image-20221018224133138

嵌入YOUTUBE視頻可參考 youtubeembedcode.com

      <div class="w-full">
        <img :src="currentImg[0]" class="w-1/2 inline-block" />
        <img :src="currentImg[1]" class="w-1/2 inline-block" />
      </div>

用於生成兩張輪播圖,每四秒切換一次,具體方法如下。註意 currentImg: function (): string[] { 可以給計算屬性添加類型檢查。

  methods: {
    startSlide: function (): void {
      this.timer = setInterval(this.next, 4000);
    },

    next: function (): void {
      this.currentIndex += 1;
    },
  },
  computed: {
    currentImg: function (): string[] {
      let index = Math.abs(this.currentIndex) % this.gameInfo.images.length;
      let index2 = (index + 1) % this.gameInfo.images.length;
      return [this.gameInfo.images[index], this.gameInfo.images[index2]];
    },
  },

考慮 img 標簽的 :src 只能接收 string ,我們假設所有 require 方法獲取的圖片均為 string 類型。定義prop類型

import { PropType } from "vue";

interface GameInfo {
  youtube: string;
  title: string;
  desc: string;
  price: number;
  platforms: string[];
  images: string[];
}

完整代碼如下

<template>
  <div
    class="bg-auto p-1 text-white"
    :style="
      'background-image:url(' + require('@/assets/diffuse/diffuse.jpg') + ')'
    "
  >
    <div class="h-52 m-2">
      <iframe
        class="h-full w-full"
        frameborder="0"
        scrolling="no"
        marginheight="0"
        marginwidth="0"
        type="text/html"
        :src="gameInfo.youtube"
      ></iframe>
    </div>
    <div class="ml-2 font-bold text-xl">{{ gameInfo.title }}</div>
    <div class="ml-2 text-sm">{{ gameInfo.desc }}.</div>
    <div class="m-2">
      <div class="w-full">
        <img :src="currentImg[0]" class="w-1/2 inline-block" />
        <img :src="currentImg[1]" class="w-1/2 inline-block" />
      </div>
    </div>
    <div class="h-8 m-2">
      <div
        class="
          inline-block
          text-black
          bg-white
          rounded-md
          text-xs
          px-1
          py-0.5
          font-bold
        "
      >
        ${{ gameInfo.price }}
      </div>
      <template v-for="(value, index) in gameInfo.platforms" :key="index">
        <component :is="value" class="inline-block ml-2"></component>
      </template>
    </div>
    <div class="h-10 m-2 w-44">
      <div
        class="
          h-full
          text-lg
          font-bold
          py-1
          px-3
          border-2 border-white
          rounded-sm
        "
      >
        <span class="inline-block">Get the game</span>
        <b-icon-arrow-right
          class="inline-block ml-1 text-lg"
        ></b-icon-arrow-right>
      </div>
    </div>
  </div>
</template>
<script lang="ts">
import { PropType } from "vue";

interface GameInfo {
  youtube: string;
  title: string;
  desc: string;
  price: number;
  platforms: string[];
  images: string[];
}

export default {
  name: "GameInfo",
  props: {
    gameInfo: {
      type: Object as PropType<GameInfo>,
      required: true,
    },
  },
  data() {
    return {
      timer: null as unknown,
      currentIndex: 0,
    };
  },
  mounted() {
    this.startSlide();
  },
  methods: {
    startSlide: function (): void {
      this.timer = setInterval(this.next, 4000);
    },

    next: function (): void {
      this.currentIndex += 1;
    },
  },
  computed: {
    currentImg: function (): string[] {
      let index = Math.abs(this.currentIndex) % this.gameInfo.images.length;
      let index2 = (index + 1) % this.gameInfo.images.length;
      return [this.gameInfo.images[index], this.gameInfo.images[index2]];
    },
  },
};
</script>

GameBlog.vue

image-20221018224814651

<template>
  <div class="m-2 mt-4">
    <div class="font-bold">From the blog</div>
    <div class="overflow-x-auto flex mt-2">
      <template v-for="(value, index) in gameBlog" :key="index">
        <div class="w-48 flex-shrink-0 mr-2">
          <img class="h-24 w-full" :src="value.img" />
          <div class="text-xs font-bold mt-1 text-stone-800 whitespace-normal">
            {{ value.title }}
          </div>
          <div class="h-12 text-xs overflow-clip mt-1 text-stone-500">
            {{ value.text }}
          </div>
        </div>
      </template>
    </div>
  </div>
</template>
<script lang="ts">
import { PropType } from "vue";

interface GameBlog {
  title: string;
  text: string;
  img: string;
}

export default {
  name: "GameBlog",
  props: {
    gameBlog: {
      type: Array as PropType<GameBlog[]>,
      required: true,
    },
  },
};
</script>

TopNavigation.vue

image-20221018224929018

<template>
  <div class="m-2 mt-4">
    <div class="font-bold inline-block">Platform & Sale</div>
    <div class="flex mt-2 flex-wrap">
      <a
        :href="value.href"
        v-for="(value, index) in platformNavigation"
        :key="index"
        class="w-1/5 flex-shrink-0 hover:text-rose-500"
      >
        <div>
          <img :src="value.img" class="w-2/5 mx-auto mt-1" />
          <div class="text-center m-1.5 text-xs">{{ value.title }}</div>
        </div>
      </a>
    </div>
  </div>
</template>
<script lang="ts">
import { PropType } from "vue";

interface platformNavigation {
  title: string;
  href: string;
  img: string;
}

export default {
  name: "PlatformNavigation",
  props: {
    platformNavigation: {
      type: Array as PropType<platformNavigation[]>,
      required: true,
    },
  },
  data() {
    return {};
  },
};
</script>

GameList.vue

image-20221018225725402

規定了比較複雜的傳入prop類型,考慮到tags可能為空,在原來的模板外層div做v-if判斷,否則會ts報錯value.tags可能為undefined。

interface Game {
  title: string;
  text: string;
  img: string;
  price: number;
  web?: boolean;
  tags?: string[];
}

interface GameList {
  title: string;
  button: {
    title: string;
    href: string;
  };
  games: Game[];
}
<div class="text-xs font-normal mt-1" v-if="value.tags">
  <template v-for="(tag, index) in value.tags" :key="index">
    <a class="text-rose-500" href="">#{{ tag }}</a>
    <template v-if="index != value.tags.length - 1">,</template>
  </template>
</div>

完整代碼

<template>
  <div class="m-2 mt-4">
    <div>
      <div class="font-bold inline-block">{{ gameList.title }}</div>

      <div v-if="gameList.button" class="float-right">
        <div
          class="
            border border-rose-400
            text-sm
            font-bold
            text-rose-500
            rounded-sm
            px-4
            py-1
            active:bg-rose-400 active:text-white
          "
        >
          {{ gameList.button.title }}
          <b-icon-arrow-right
            class="inline-block text-lg align-text-top"
          ></b-icon-arrow-right>
        </div>
      </div>

      <div class="w-full mt-4 flex flex-wrap justify-between">
        <template v-for="(value, index) in gameList.games" :key="index">
          <div class="w-44 inline-block align-top">
            <img class="h-28 w-full" :src="value.img" />
            <div
              class="text-xs font-bold mt-1 text-stone-800 w-3/4 inline-block"
            >
              {{ value.title }}
            </div>
            <div
              class="
                inline-block
                w-1/4
                align-top
                text-xs
                bg-stone-200
                rounded-sm
                py-0.5
                mt-1
                text-center
                font-bold
              "
              :class="{ 'bg-stone-500': value.price != 0 }"
            >
              <span v-if="value.web">WEB</span>
              <span v-else-if="value.price == 0">FREE</span>
              <span v-else-if="value.price != 0" class="font-normal text-white"
                >${{ value.price }}</span
              >
            </div>
            <div class="text-xs font-normal mt-1" v-if="value.tags">
              <template v-for="(tag, index) in value.tags" :key="index">
                <a class="text-rose-500" href="">#{{ tag }}</a>
                <template v-if="index != value.tags.length - 1">,</template>
              </template>
            </div>
            <div class="text-xs font-normal text-stone-500 mt-1">
              {{ value.text }}
            </div>
            <div class="my-1"></div>
          </div>
        </template>
      </div>
    </div>
  </div>
</template>
<script lang="ts">
import { PropType } from "vue";

interface Game {
  title: string;
  text: string;
  img: string;
  price: number;
  web?: boolean;
  tags?: string[];
}

interface GameList {
  title: string;
  button: {
    title: string;
    href: string;
  };
  games: Game[];
}

export default {
  name: "GameList",
  props: {
    gameList: {
      type: Object as PropType<GameList>,
      required: true,
    },
  },
};
</script>

HomeFooter.vue

<template>
  <div class="mx-2 my-4">
    <div class="text-center font-bold text-sm">
      Don't see anything you like?
    </div>
    <div
      class="
        w-11/12
        h-10
        pt-2.5
        text-center
        m-auto
        mt-4
        border border-rose-500
        font-bold
        text-sm text-rose-500
      "
    >
      View all Games
      <b-icon-arrow-right
        class="inline-block text-lg align-text-top"
      ></b-icon-arrow-right>
    </div>
    <div
      class="
        w-11/12
        h-10
        pt-2.5
        text-center
        m-auto
        mt-4
        border border-rose-500
        font-bold
        text-sm text-rose-500
      "
    >
      View something random
      <b-icon-arrow-left-right
        class="inline-block text-lg align-text-top"
      ></b-icon-arrow-left-right>
    </div>
  </div>
</template>
<script>
export default {
  name: "HomeFooter",
  props: {
  }
}
</script>

幾個問題

這裡列舉我在開發過程遇到的一些問題,也許能幫助到你。

ERROR Error: The project seems to require yarn but it's not installed.

明明 yarn serve 成功了,並顯示如下內容,但連接網頁還是轉圈圈。嘗試重啟電腦後重新 yarn serve

  App running at:
  - Local:   http://localhost:8080/ 

得到如下報錯

 ERROR  Error: The project seems to require yarn but it's not installed.

解決方法:刪除當前目錄下的 yarn.lock 文件,命令行輸入 npm install -g yarn


Type assertion expressions can only be used in TypeScript files.Vetur(8016)

解決方法:修改 <script> 為 <script lang="ts">


網站資源

Bootstrap Icons · Official open source SVG icon library for Bootstrap (getbootstrap.com)

Working with props declaration in Vue 3 + Typescript - DEV Community

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、什麼是工作流? 在闡述什麼是工作流之前,先說一下工作流和普通任務的區別,在於依賴視圖。 普通任務本身他只會有自己的dag圖,依賴視圖是無邊界的,不可控的,而工作流則是把整個工作流都展示出來,是有邊界的,可控的,這是工作流的優勢。下麵為大家介紹工作流的相關功能: 01 工作流—功能介紹 ● 虛擬節 ...
  • GreatSQL社區原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。 GreatSQL是MySQL的國產分支版本,使用上與MySQL一致。 本文來源:原創投稿;作者:YeJinrong/葉金榮 測試效率提升36% ~ 100%,相當可觀 本文目錄 並行構建索引測試 進一步提高索引構建效率 並 ...
  • 背景 企業資料庫選型規則。 一、資料庫部署形式 隨著硬體發展,指標上會有變化。 部署形式決定了容量上限,計算能力上限,讀寫帶寬上限,RPO,RTO指標,適應場景。 1、分散式部署(例如pg+citus插件) 容量上限:100節點以上,PB級。 計算能力上限:100節點以上,6400核以上。 讀寫帶寬 ...
  • 複製集群 &.主從模式 (讀寫分離) 主從模式原理 集群運作原理 Redis主從複製預設讀寫分離(主寫從讀). 單點故障時,預設的容災機制可以實現快速故障恢復(單點/多點故障). 主從集群說明 (優點) 易擴展 (可以動態的添加增加從機) (優點) 讀寫分離 (主寫從讀) (缺點) 複製延遲 (寫操 ...
  • 本篇記錄我在實現時的思考過程,寫給之後可能遇到困難的我自己也給到需要幫助的人。 寫的比較淺顯,見諒。 在寫項目代碼的時候,需要把Android端的位置信息傳輸到伺服器端,通過Netty達到連續傳輸的效果,如下: 我們可以先來看看百度地圖官方給出的相關代碼 public class MainActiv ...
  • 原文地址:Android自動化測試工具調研 - Stars-One的雜貨小窩 Android測試按測試方式分類,可分為兩種:一種是傳統邏輯單元測試(Junit),另外一種則是UI交互頁面測試。 這裡詳細講解第二種測試。 UI交互頁面測試如果是人工進行,會消耗人力,且不一定按質量進行測試,測試不全面, ...
  • 第三期 · 使用 Vue 3.1 + Axios + Golang + Sqlite3 實現簡單評論機制 效果圖 CommentArea.vue 我們需要藉助js的Data對象把毫秒時間戳轉化成 UTCString() 。併在模板表達式中使用 {{ dateConvert(value.date) } ...
  • 問題 ios設備:點擊input,軟鍵盤彈出,頁面整體向上偏移 需求 當軟鍵盤彈起,input改變位置並始終貼著軟鍵盤,整體頁面不上移動 解決 頁面採用flex佈局 <div class="flex"> <div class="box"> <div class="head"></div> //標題區 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...