Vue.js でダッシュボードを作成①(レイアウト作成〜Sass導入編)

Webアプリケーションを作成するとき、レイアウトはいつも Bootstrap などの CSS フレームワークを使っていたのですが、CSS の理解を深めるためにフレームワークを使わず CSS のみで実装してみました。
今回は JavaScript フレームワークに Vue.js を使い、コンポーネント設計は Atomic Design を取り入れてみました。





*作ったもの

下記の画面を作成しました。
まだ画面遷移や動的な表示といった処理を行うことはできません。

















*環境

  • MacOS
  • vue/cli 3.8.4
  • vue-router 3.0.6
  • node-sass 4.12.0
  • sass-loader 7.1.0
  • axios 0.19.0


*環境構築

下記記事の「Vue CLI 3 のインストール 〜 Vue router の導入」を行い、Vueプロジェクトを作成します。


*Sass の導入

今回は Sass を使いたいので下記コマンドを実行して Vue ファイルから Sass を使えるようにします。
$ npm install --save node-sass sass-loader axios

プロジェクト作成と同時に生成されるHelloWorld.vueで Sass の動作確認をしてみます。
<style scoped>lang="scss"を追加し、stylehello__footerクラスを追加します。
次に<template>のなかにhello__footerクラスを適用したdiv要素を追加してアプリケーションを起動すると、画面下に「footer」の文字が青色で表示されます。
<HelloWorld.vue>
<template>
 ...
 
    <h3>Ecosystem</h3>
    <ul>
      <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
      <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
      <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
      <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
    </ul>
    <!---- ↓追加 ---->
    <div class="hello__footer">footer</div>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  props: {
    msg: String
  }
};
</script>

<!---- ↓ langを追加 ---->
<style scoped lang="scss">
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
// ---- ↓追加 ----
.hello {
  &__footer {
    color: #0000ff;
  }
}
</style>


*コンポーネント設計

Atomic Design を参考に下記構成にしました。
より最適な設計があるかもしれないです。
manage-app
├── README.md
├── babel.config.js
├── node_modules
├── package-lock.json
├── package.json
├── public
└── src
 ├── App.vue
 ├── assets
 │   └── logo.png
 ├── components
 │   ├── atoms
 │   │   ├── AppLogo.vue
 │   │   ├── CardNumber.vue
 │   │   ├── CardTitle.vue
 │   │   └── LoginLink.vue
 │   ├── molecules
 │   │   ├── GraphCard.vue
 │   │   ├── ListCard.vue
 │   │   └── ValueCard.vue
 │   ├── organisms
 │   │   ├── Navbar.vue
 │   │   └── Sidebar.vue
 │   ├── pages
 │   │   ├── Dashboard.vue
 │   │   └── HelloWorld.vue
 │   └── templates
 ├── main.js
 └── router
     └── index.js


*ダッシュボード画面

メインとなるダッシュボード画面を実装します。
この画面を親コンポーネントとし、ナビゲーションバーなどの子コンポーネントを呼び出します。ナビゲーションバーとサイドバーはスクロールしても画面から外れないよう固定表示にしたので、コンテンツ表示部はその分をpaddingで調整しています。

子コンポーネントを使う際は、<script>内で対象ファイルをimportし、componentsオプションとして登録します。そうすることで登録したコンポーネントをタグとして使うことができるようになります。(タグとして使うときはケバブケースにします)
また、プロパティを使って子コンポーネントに値を渡すこともできます。
<src/components/pages/Dashboard.vue>
<template>
  <div>
    <navbar></navbar>
    <sidebar></sidebar>
    <div class="top">
      <div>
        <value-card name="ユーザー数 合計" value="256"></value-card>
        <value-card name="プレビュー数 合計" value="21,234"></value-card>
      </div>
      <graph-card name="ユーザー数 推移"></graph-card>
      <graph-card name="プレビュー数 推移"></graph-card>
      <list-card name="データ一覧"></list-card>
    </div>
  </div>
</template>
<script>
import Navbar from "../organisms/Navbar";
import Sidebar from "../organisms/Sidebar";
import ValueCard from "../molecules/ValueCard";
import GraphCard from "../molecules/GraphCard";
import ListCard from "../molecules/ListCard";
export default {
  components: {
    Navbar,
    Sidebar,
    ValueCard,
    GraphCard,
    ListCard
  }
};
</script>
<style scoped lang="scss">
.top {
  padding: 66px 0 0 200px;
}
</style>

プロジェクト作成時のままだと http://localhost:8080 にアクセスした際に HelloWorld.vue が呼び出されてしまうので、DashBoard.vue が呼ばれるよう index.js を修正します。
<src/router/index.js>
import Vue from "vue";
import Router from "vue-router";
import Dashboard from "@/components/pages/Dashboard";

Vue.use(Router);

export default new Router({
  mode: "history",
  routes: [
    {
      path: "/",
      name: "Dashboard",
      component: Dashboard
    }
  ]
});


*ナビゲーションバー

画面の上部に表示するナビゲーションバーを実装します。




スクロールしてもナビゲーションバーがついてくるようposition: fixedを使って固定しています。メニューについては箇条書きに使うul, liタグを使い、display: inline-blockを使って要素を横並びにしています。このままだとliタグに箇条書きの「・」がついたままになってしまうので、list-style: noneを使って消しています。

また、ロゴとログインユーザー名は別コンポーネントとして読み込んでいます。
<src/components/organisms/Navbar.vue>
<template>
  <nav class="nav-header">
    <ul class="nav-header__menu">
      <app-logo value="Web Analysis"></app-logo>
      <li class="current"><a href="#">ホーム</a></li>
      <li><a href="#">月毎</a></li>
      <li><a href="#">日毎</a></li>
      <login-link :value="`admin`"></login-link>
    </ul>
  </nav>  
</template>
<script>
import AppLogo from "../atoms/AppLogo";
import LoginLink from "../atoms/LoginLink";
export default {
  components: {
    AppLogo,
    LoginLink
  }
};
</script>
<style scoped lang="scss">
.nav-header {
  text-align: center;
  height: 67px;
  background-color: white;
  position: fixed;
  width: 100%;
}
.nav-header__menu {
  padding: 20px 0;
  margin: 0;
  text-align: left;

  & li {
    list-style: none;
    display: inline-block;
    width: 200px;
    min-width: 90px;
    text-align: center;
  }
  & li a {
    text-decoration: none;
    color: #333;
  }
  & li.current a {
    color: #3280f5;
  }
  & li a:hover {
    color: #b1b6be;
  }
}
</style>

ナビゲーションバー左上のロゴはatomsコンポーネントとして切り出しました。
propsで受け取った値をロゴ名として表示しています。
<src/components/atoms/AppLogo.vue>
<template>
  <li class="logo-label">{{ value }}</li>
</template>
<script>
export default {
  props: ["value"]
};
</script>
<style scoped lang="scss">
.logo-label {
  font-size: 20px;
  font-weight: bold;
  width: 200px;
  vertical-align: center;
  color: #3373d3;
  border-right: 2px solid #ddd;
}
</style>

ナビゲーションバー右のログインユーザー名を表示する部分は、propsで親からユーザー名を受け取って表示するようにしています。
また、ユーザー名をクリックするとログアウトできるようアコーディオンメニューにしました。これはチェックボックスを隠し持っておき、ユーザー名がクリックされたタイミングで表示/非表示の切り替えを行います。









<src/components/atoms/LoginLink.vue>
<template>
  <li class="login">
    <input id="login-check1" class="login__check" type="checkbox">
    <label class="login__label" for="login-check1">{{ value }}</label>
    <div class="login__content">
      <div class="login__menu">
        <a class="login__link" href="#">ログアウト</a>
      </div>
    </div>
  </li>
</template>
<script>
export default {
  props: ["value"]
};
</script>
<style scoped lang="scss">
.login {
  width: 200px;
  position: absolute;
  right: 0;
  border-left: 2px solid #ddd;
  height: 27px;

  &__content {
    box-shadow: 0 5px 3px -3px #ddd;
    background-color: white;
    height: 0;
    opacity: 0;
    padding: 0 12px;
    transition: 0.5s;
    visibility: hidden;
    border-radius: 4px;
  }
  &__menu {
    padding: 10px;
    transition: 0.8s;
  }
  &__link {
    color: #333;
    text-decoration: none;
  }
  &__check {
    display: none;
  }
  &__label {
    width: 200px;
    display: inline-block;
    cursor: pointer;
  }
  &__check:checked + &__label + &__content {
    height: 40px;
    opacity: 1;
    padding: 10px;
    visibility: visible;
  }
}
</style>


*サイドバー

画面の左に表示するサイドバーを実装します。
項目にカーソルを当てると色が変わるようにしています。



















<src/components/organisms/Sidebar.vue>
<template>
  <ul class="side-menu">
    <li><a href="#">ダッシュボード</a></li>
    <li><a href="#">推移グラフ</a></li>
    <li><a href="#">結果レポート</a></li>
    <li><a href="#">設定</a></li>
    <li><a href="#">問い合わせ</a></li>
  </ul>
</template>
<script>
export default {};
</script>
<style scoped lang="scss">
.side-menu {
  top: 74px;
  position: fixed;
  width: 200px;
  height: 100vmax;
  float: left;
  background-color: #3373d3;
  margin: 0;
  padding: 15px 0 0 0;

  & li a {
    color: white;
    font-weight: bold;
    text-decoration: none;
  }
  & li {
    padding: 25px 0 25px 35px;
    list-style: none;
  }
  & li:hover {
    background-color: #6ea3e9cb;
  }
}
</style>


*カード

コンテンツとして表示するカードを実装します。









タイトルとカード内部は複数箇所で使うので、別コンポーネントに切り出しました。
親コンポーネントからカードタイトルと表示内容をpropsで受け取り、その値をさらにvalueという名前で子コンポーネントに渡しています。
<src/components/molecules/ValueCard.vue>
<template>
  <div class="value-content">
    <card-title :value="name"></card-title>
    <card-number :value="value"></card-number>
  </div>
</template>
<script>
import CardTitle from "../atoms/CardTitle";
import CardNumber from "../atoms/CardNumber";
export default {
  components: {
    CardTitle,
    CardNumber
  },
  props: {
    name: String,
    value: String
  }
};
</script>
<style scoped lang="scss">
.value-content {
  display: inline-block;
  margin: 40px 0 40px 40px;
  height: 130px;
  width: 350px;
  background-color: white;
  border: 0 solid #aaa;
  border-radius: 4px;
  box-shadow: 2px 2px 2px 2px #ccc;
  padding: 15px;
}
</style>

カードのタイトルを atomsコンポーネントにし、親からpropsで受け取った値を表示しています。
<src/components/atoms/CardTitle.vue>
<template>
  <div class="title-label">{{ value }}</div>
</template>
<script>
export default {
  props: ["value"]
};
</script>
<style scoped lang="scss">
.title-label {
  font-size: 20px;
  font-weight: bold;
  color: grey;
  padding-bottom: 3px;
  border-bottom: solid 2px #3280f57e;
}
</style>

カードの数値だけを表示する部分も atomsコンポーネントにしています。
<src/components/atoms/CardNumber.vue>
<template>
  <div class="number-label">{{ value }}</div>
</template>
<script>
export default {
  props: ["value"]
};
</script>
<style scoped lang="scss">
.number-label {
  text-align: center;
  margin-top: 30px;
  font-size: 40px;
}
</style>


*所感

CSSフレームワークは便利ですが、微調整がしにくかったり似たデザインになってしまうといったデメリットもあるので、CSS だけで柔軟な実装できるよう理解を深めていきたいと思っています。
モバイルやタブレットの画面サイズの対応や、他画面のレイアウト、動的な処理の実装はこれから追加していきます。

Previous
Next Post »

人気の投稿