<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>六角學院 &#8211; wordpress_blog</title>
	<atom:link href="/wordpress_blog/category/hexschool/feed/" rel="self" type="application/rss+xml" />
	<link>/wordpress_blog</link>
	<description>This is a dynamic to static website.</description>
	<lastBuildDate>Wed, 26 Mar 2025 00:59:40 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.7.2</generator>

<image>
	<url>/wordpress_blog/wp-content/uploads/2022/03/logo.png</url>
	<title>六角學院 &#8211; wordpress_blog</title>
	<link>/wordpress_blog</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Vue3 複習10</title>
		<link>/wordpress_blog/reviewvue3_10/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Tue, 06 Aug 2024 06:24:43 +0000</pubDate>
				<category><![CDATA[六角學院]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=870</guid>

					<description><![CDATA[從頭開始 – Pinia 製作一個購物車 2023 新增章節 P [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">從頭開始 – Pinia 製作一個購物車 2023 新增章節</h2>



<h3 class="wp-block-heading">Pinia 相關資源</h3>



<ul class="wp-block-list">
<li><a href="https://pinia.vuejs.org/" target="_blank" rel="noreferrer noopener">Pinia 官方文件</a></li>



<li><a href="https://www.youtube.com/watch?v=_vFuDQ_6Xt8" target="_blank" rel="noreferrer noopener">陣列方法補充</a></li>



<li>課程 Pinia CDN 範例</li>
</ul>



<pre class="wp-block-code"><code>// 課程 Pinia CDN 範例
&lt;!-- VueDemi，使用 Pinia 必要的相依套件 --&gt;
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/vue-demi/0.13.11/index.iife.js"&gt;&lt;/script&gt;
&lt;script&gt;const I = VueDemi; const vueDemi = VueDemi;&lt;/script&gt;

&lt;!-- Pinia 網頁版，實戰中還是以 npm 為主，這是比較少見的使用方式 --&gt;
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/pinia/2.0.22/pinia.iife.js"&gt;&lt;/script&gt;</code></pre>



<h3 class="wp-block-heading">01. pinia 簡介</h3>



<h4 class="wp-block-heading">元件資料獨立與傳遞</h4>



<h4 class="wp-block-heading">頁面元件結構</h4>



<h4 class="wp-block-heading">Pinia 好處</h4>



<ul class="wp-block-list">
<li>跨元件的狀態、方法管理</li>



<li>易於學習，許多觀念與 Vue.js 連貫</li>



<li>相對於其他狀態管理工具更容易上手</li>
</ul>



<h3 class="wp-block-heading">02. 專案簡介</h3>



<ul class="wp-block-list">
<li>完成版型製作</li>



<li>將產品資料渲染至畫面上</li>



<li>最終範例</li>
</ul>



<h3 class="wp-block-heading">03. 版型製作</h3>



<ul class="wp-block-list">
<li>新增 layout.html 檔案</li>



<li>開啟 Bootstrap 5 文件<br>Components > Navbar > Text<br>選擇單純的結構複製程式碼</li>



<li>修改 layout.html 檔案<br>品牌名稱、按鈕<br>Components > Buttons<br>選擇單純的結構複製程式碼</li>



<li>在購物車的地方加上數字<br>Components > Badge > Pill Badges，選擇 danger 的顏色<br>修改 Badge 的 class</li>



<li>完成購物車版型<br>Form > Select<br>Utilities > Vertical align</li>



<li>製作產品卡片結構<br>Components > Card</li>
</ul>



<pre class="wp-block-code"><code>// pinia/layout.html
&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="UTF-8" /&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0" /&gt;
    &lt;title&gt;完整版型製作&lt;/title&gt;
    &lt;!-- Bootstarp 5 CSS CDN --&gt;
    &lt;link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
    /&gt;
    &lt;style&gt;
      .table-image {
        height: 100px;
        width: 100px;
        object-fit: cover;
      }

      .card-img-top {
        height: 200px;
        object-fit: cover;
      }
    &lt;/style&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id="app"&gt;
      &lt;div class="container py-5"&gt;
        &lt;h2&gt;完成版型製作&lt;/h2&gt;
        &lt;nav class="navbar bg-body-tertiary"&gt;
          &lt;div class="container-fluid"&gt;
            &lt;span class="navbar-brand mb-0 h1"&gt;香香餅乾店&lt;/span&gt;
            &lt;button type="button" class="btn"&gt;購物車
              &lt;span class="badge rounded-pill bg-danger text-white"&gt;0&lt;/span&gt;
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/nav&gt;

        &lt;div class="bg-light my-4 p-4"&gt;
          &lt;div&gt;購物車沒有任何品項&lt;/div&gt; &lt;!-- v-if --&gt;
          &lt;!-- v-else --&gt;
          &lt;table class="table align-middle"&gt;
          &lt;tbody&gt;
            &lt;tr&gt;
              &lt;td&gt;
                &lt;a href="#" class="text-dark"&gt;x&lt;/a&gt;
              &lt;/td&gt;
              &lt;td&gt;
                &lt;img src="https://images.unsplash.com/photo-1597733153203-a54d0fbc47de?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1090&amp;q=80" class="table-image" alt=""&gt;
              &lt;/td&gt;
              &lt;td&gt;好吃的餅乾&lt;/td&gt;
              &lt;td&gt;
                &lt;select name="" id="" class="form-select"&gt;
                  &lt;option value=""&gt;1&lt;/option&gt;
                &lt;/select&gt;
              &lt;/td&gt;
              &lt;td class="text-end"&gt;
                $900
              &lt;/td&gt;
            &lt;/tr&gt;
          &lt;/tbody&gt;
          &lt;tfoot&gt;
            &lt;td colspan="5" class="text-end"&gt;總金額 NT$ 900&lt;/td&gt;
          &lt;/tfoot&gt;
          &lt;/table&gt;
        &lt;/div&gt;

        &lt;div class="row row-cols-3 my-4"&gt;
          &lt;div class="col"&gt;
            &lt;div class="card"&gt;
              &lt;img src="https://images.unsplash.com/photo-1597733153203-a54d0fbc47de?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1090&amp;q=80"
              class="card-img-top" alt=""&gt;
              &lt;div class="card-body"&gt;
                &lt;h6 class="card-title"&gt;好吃的餅乾
                  &lt;span class="float-end"&gt;$900&lt;/span&gt;
                &lt;/h6&gt;
                &lt;a href="#" class="btn btn-outline-primary w-100"&gt;加入購物車&lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- Vue 3 CDN --&gt;
    &lt;script src="https://unpkg.com/vue@3/dist/vue.global.js"&gt;&lt;/script&gt;
    &lt;!-- Bootstrap 5 JS CDN --&gt;
    &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"&gt;&lt;/script&gt;
    &lt;script type="module"&gt;
      const app = Vue.createApp({
        // 資料 (函式)
        data() {
          return {};
        },
        // 生命週期 (函式)
        created() {
          console.log(this);
        },
        // 方法 (物件)
        methods: {},
      });

      app.mount("#app");
    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>



<h3 class="wp-block-heading">04. 轉換vue元件</h3>



<ul class="wp-block-list">
<li>修改 layout.html 檔案，增加 script 片段</li>



<li>新增 homeworkComponents 資料夾</li>



<li>製作 Navbar 元件，避免出錯可以先直接在目前的專案位置進行撰寫</li>



<li>新增 navbarComponent.js 檔案</li>



<li>修改 navbarComponent.js 檔案</li>



<li>修改 layout.html 檔案，匯入 NavbarComponent</li>



<li>新增 cartComponent.js 檔案</li>



<li>修改 cartComponent.js 檔案</li>



<li>修改 layout.html 檔案，匯入 CartComponent</li>



<li>新增 productsComponent.js 檔案</li>



<li>修改 prodcutsComponent.js 檔案</li>



<li>修改 layout.html 檔案，匯入 ProductComponent</li>
</ul>



<pre class="wp-block-code"><code>// pinia/layout.html
&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="UTF-8" /&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0" /&gt;
    &lt;title&gt;完整版型製作&lt;/title&gt;
    &lt;!-- Bootstarp 5 CSS CDN --&gt;
    &lt;link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
    /&gt;
    &lt;style&gt;
      .table-image {
        height: 100px;
        width: 100px;
        object-fit: cover;
      }

      .card-img-top {
        height: 200px;
        object-fit: cover;
      }
    &lt;/style&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id="app"&gt;
      &lt;div class="container py-5"&gt;
        &lt;h2&gt;完成版型製作&lt;/h2&gt;

        &lt;Navbar-Component&gt;&lt;/Navbar-Component&gt;

        &lt;Cart-Component&gt;&lt;/Cart-Component&gt;

        &lt;Product-Component&gt;&lt;/Product-Component&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- Vue 3 CDN --&gt;
    &lt;script src="https://unpkg.com/vue@3/dist/vue.global.js"&gt;&lt;/script&gt;
    &lt;!-- Bootstrap 5 JS CDN --&gt;
    &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"&gt;&lt;/script&gt;
    &lt;script type="module"&gt;
      const { createApp } = Vue;

      import NavbarComponent from './homeworkComponents/navbarComponent.js'
      import CartComponent from './homeworkComponents/cartComponent.js'
      import ProductComponent from './homeworkComponents/productsComponent.js'

      const app =createApp({
        components: {
          NavbarComponent,
          CartComponent,
          ProductComponent
        }
      }).mount('#app');

    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/navbarComponent.js

export default {
  template: `&lt;nav class="navbar bg-body-tertiary"&gt;
    &lt;div class="container-fluid"&gt;
      &lt;span class="navbar-brand mb-0 h1"&gt;香香餅乾店&lt;/span&gt;
      &lt;button type="button" class="btn"&gt;購物車
        &lt;span class="badge rounded-pill bg-danger text-white"&gt;0&lt;/span&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/nav&gt;`
}
</code></pre>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/cartComponent.js

export default {
  template: `&lt;div class="bg-light my-4 p-4"&gt;
    &lt;div&gt;購物車沒有任何品項&lt;/div&gt; &lt;!-- v-if --&gt;
    &lt;!-- v-else --&gt;
    &lt;table class="table align-middle"&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;
          &lt;a href="#" class="text-dark"&gt;x&lt;/a&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;img src="https://images.unsplash.com/photo-1597733153203-a54d0fbc47de?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1090&amp;q=80" class="table-image" alt=""&gt;
        &lt;/td&gt;
        &lt;td&gt;好吃的餅乾&lt;/td&gt;
        &lt;td&gt;
          &lt;select name="" id="" class="form-select"&gt;
            &lt;option value=""&gt;1&lt;/option&gt;
          &lt;/select&gt;
        &lt;/td&gt;
        &lt;td class="text-end"&gt;
          $900
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
    &lt;tfoot&gt;
      &lt;td colspan="5" class="text-end"&gt;總金額 NT$ 900&lt;/td&gt;
    &lt;/tfoot&gt;
    &lt;/table&gt;
  &lt;/div&gt;`
}
</code></pre>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/productsComponent.js

export default {
  template: `&lt;div class="row row-cols-3 my-4"&gt;
    &lt;div class="col"&gt;
      &lt;div class="card"&gt;
        &lt;img src="https://images.unsplash.com/photo-1597733153203-a54d0fbc47de?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1090&amp;q=80"
        class="card-img-top" alt=""&gt;
        &lt;div class="card-body"&gt;
          &lt;h6 class="card-title"&gt;好吃的餅乾
            &lt;span class="float-end"&gt;$900&lt;/span&gt;
          &lt;/h6&gt;
          &lt;a href="#" class="btn btn-outline-primary w-100"&gt;加入購物車&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;`
}
</code></pre>



<h3 class="wp-block-heading">05. 導入產品資料</h3>



<ul class="wp-block-list">
<li>從範例程式碼 productComponet.js 檔案複製產品資料</li>



<li>修改 productsComponent.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/productsComponent.js

export default {
  data() {
    return {
      products: &#91;
        {
          id: 1,
          title: '多色餅乾',
          imageUrl: 'https://images.unsplash.com/photo-1576717585968-8ea8166b89b8?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
          price: 80
        },
        {
          id: 2,
          title: '綠色馬卡龍',
          imageUrl: 'https://images.unsplash.com/photo-1623066463831-3f7f6762734d?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1135&amp;q=80',
          price: 120
        },
        {
          id: 3,
          title: '甜蜜左擁右抱',
          imageUrl: 'https://images.unsplash.com/photo-1558312657-b2dead03d494?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
          price: 200,
        },
        {
          id: 4,
          title: '巧克力心連心',
          imageUrl: 'https://images.unsplash.com/photo-1606913084603-3e7702b01627?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
          price: 160
        },
        {
          id: 5,
          title: '粉係馬卡龍',
          imageUrl: 'https://images.unsplash.com/photo-1612201142855-7873bc1661b4?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
          price: 120
        }
      ]
    }
  },
  template: `&lt;div class="row row-cols-3 my-4 g-4"&gt;
    &lt;div class="col" v-for="product in products" :key="product.id"&gt;
      &lt;div class="card"&gt;
        &lt;img :src="product.imageUrl"
        class="card-img-top" alt=""&gt;
        &lt;div class="card-body"&gt;
          &lt;h6 class="card-title"&gt;{{ product.title }}
            &lt;span class="float-end"&gt;$ {{ product.price }}&lt;/span&gt;
          &lt;/h6&gt;
          &lt;a href="#" class="btn btn-outline-primary w-100"&gt;加入購物車&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;`
}
</code></pre>



<h3 class="wp-block-heading">06. 建立 store</h3>



<ul class="wp-block-list">
<li>修改 layout.html 檔案，複製範例片段程式碼、載入套件程式碼，實戰中比較少用這種方式</li>



<li>使用 log 查詢 Pinia 是否有正確匯入</li>



<li>在 pinia 資料夾裡面建立 store 資料夾</li>



<li>在 store 資料夾裡面建立 productsStore.js 檔案</li>



<li>會使用到的 layout.html、productsComponent.js、productsStore.js 檔案</li>



<li>修改 layout.html 檔案，匯入 productStore</li>



<li>修改 productsStore.js 檔案</li>



<li>修改 productsComponent.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// pinia/layout.html
&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="UTF-8" /&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0" /&gt;
    &lt;title&gt;完整版型製作&lt;/title&gt;
    &lt;!-- Bootstarp 5 CSS CDN --&gt;
    &lt;link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
    /&gt;
    &lt;style&gt;
      .table-image {
        height: 100px;
        width: 100px;
        object-fit: cover;
      }

      .card-img-top {
        height: 200px;
        object-fit: cover;
      }
    &lt;/style&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id="app"&gt;
      &lt;div class="container py-5"&gt;
        &lt;h2&gt;完成版型製作&lt;/h2&gt;

        &lt;Navbar-Component&gt;&lt;/Navbar-Component&gt;

        &lt;Cart-Component&gt;&lt;/Cart-Component&gt;

        &lt;Product-Component&gt;&lt;/Product-Component&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- Vue 3 CDN --&gt;
    &lt;script src="https://unpkg.com/vue@3/dist/vue.global.js"&gt;&lt;/script&gt;
    &lt;!-- Bootstrap 5 JS CDN --&gt;
    &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"&gt;&lt;/script&gt;
    &lt;!-- VueDemi，使用 Pinia 必要的相依套件 --&gt;
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/vue-demi/0.13.11/index.iife.js"&gt;&lt;/script&gt;
    &lt;script&gt;const I = VueDemi; const vueDemi = VueDemi;&lt;/script&gt;
    &lt;!-- Pinia 網頁版，實戰中還是以 npm 為主，這是比較少見的使用方式 --&gt;
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/pinia/2.0.22/pinia.iife.js"&gt;&lt;/script&gt;
    &lt;script type="module"&gt;
      const { createApp } = Vue;
      const { createPinia } = Pinia;

      import NavbarComponent from './homeworkComponents/navbarComponent.js'
      import CartComponent from './homeworkComponents/cartComponent.js'
      import ProductComponent from './homeworkComponents/productsComponent.js'

      const app =createApp({
        components: {
          NavbarComponent,
          CartComponent,
          ProductComponent
        }
      })

      const pinia = createPinia()
      app.use(pinia)      
      app.mount('#app')

    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>



<pre class="wp-block-code"><code>// pinia/store/productsStore.js
const { defineStore } = Pinia;

export default defineStore('productsStore', {
  // data, methods, computed
  // state, action, getters
  state: () =&gt; ({
    products: &#91;
      {
        id: 1,
        title: '多色餅乾',
        imageUrl: 'https://images.unsplash.com/photo-1576717585968-8ea8166b89b8?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
        price: 80
      },
      {
        id: 2,
        title: '綠色馬卡龍',
        imageUrl: 'https://images.unsplash.com/photo-1623066463831-3f7f6762734d?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1135&amp;q=80',
        price: 120
      },
      {
        id: 3,
        title: '甜蜜左擁右抱',
        imageUrl: 'https://images.unsplash.com/photo-1558312657-b2dead03d494?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
        price: 200,
      },
      {
        id: 4,
        title: '巧克力心連心',
        imageUrl: 'https://images.unsplash.com/photo-1606913084603-3e7702b01627?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
        price: 160
      },
      {
        id: 5,
        title: '粉係馬卡龍',
        imageUrl: 'https://images.unsplash.com/photo-1612201142855-7873bc1661b4?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
        price: 120
      }
    ]
  }),
  getters: {
    sortProducts: ({ products }) =&gt; {
      return products 
    }
  }
})
</code></pre>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/productsComponent.js
import productsStore from "../store/productsStore.js"
const { mapState } = Pinia

export default {
  data() {
    return {
      
    }
  },
  template: `&lt;div class="row row-cols-3 my-4 g-4"&gt;
    &lt;div class="col" v-for="product in sortProducts" :key="product.id"&gt;
      &lt;div class="card"&gt;
        &lt;img :src="product.imageUrl"
        class="card-img-top" alt=""&gt;
        &lt;div class="card-body"&gt;
          &lt;h6 class="card-title"&gt;{{ product.title }}
            &lt;span class="float-end"&gt;$ {{ product.price }}&lt;/span&gt;
          &lt;/h6&gt;
          &lt;a href="#" class="btn btn-outline-primary w-100"&gt;加入購物車&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;`,
  computed: {
    ...mapState(productsStore, &#91;'sortProducts'])
  }
}
</code></pre>



<h3 class="wp-block-heading">07. 建立購物車 Store</h3>



<ul class="wp-block-list">
<li>修改 productsStore.js 檔案</li>



<li>加入購物車的行為<br>在 store 資料夾裡面建立 cartStore.js 檔案，專門管理購物車所有方法</li>



<li>修改 cartStore.js 檔案</li>



<li>修改 productsComponent 檔案，匯入 cartStore</li>



<li>修改 cartStore.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// pinia/store/productsStore.js
const { defineStore } = Pinia;

export default defineStore('productsStore', {
  // data, methods, computed
  // state, action, getters
  state: () =&gt; ({
    products: &#91;
      {
        id: 1,
        title: '多色餅乾',
        imageUrl: 'https://images.unsplash.com/photo-1576717585968-8ea8166b89b8?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
        price: 80
      },
      {
        id: 2,
        title: '綠色馬卡龍',
        imageUrl: 'https://images.unsplash.com/photo-1623066463831-3f7f6762734d?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1135&amp;q=80',
        price: 120
      },
      {
        id: 3,
        title: '甜蜜左擁右抱',
        imageUrl: 'https://images.unsplash.com/photo-1558312657-b2dead03d494?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
        price: 200,
      },
      {
        id: 4,
        title: '巧克力心連心',
        imageUrl: 'https://images.unsplash.com/photo-1606913084603-3e7702b01627?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
        price: 160
      },
      {
        id: 5,
        title: '粉係馬卡龍',
        imageUrl: 'https://images.unsplash.com/photo-1612201142855-7873bc1661b4?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
        price: 120
      }
    ]
  }),
  getters: {
    sortProducts: ({ products }) =&gt; {
      return products.sort((a, b) =&gt; a.price - b.price)
    }
  }
})
</code></pre>



<pre class="wp-block-code"><code>// pinia/store/cartStore.js
const { defineStore } = Pinia;

export default defineStore('cart', {
  // methods
  // actions
  state: () =&gt; ({
    cart: &#91;]
  }),
  actions: {
    addToCart(productId, qty = 1) {
      console.log(productId, qty);
      this.cart.push({
        id: new Date().getTime(),
        productId,
        qty
      })
      console.log(this.cart);
    }
  }
})
</code></pre>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/productsComponent.js
import productsStore from "../store/productsStore.js"
import cartStore from '../store/cartStore.js'
const { mapState, mapActions } = Pinia

export default {
  data() {
    return {
      
    }
  },
  template: `&lt;div class="row row-cols-3 my-4 g-4"&gt;
    &lt;div class="col" v-for="product in sortProducts" :key="product.id"&gt;
      &lt;div class="card"&gt;
        &lt;img :src="product.imageUrl"
        class="card-img-top" alt=""&gt;
        &lt;div class="card-body"&gt;
          &lt;h6 class="card-title"&gt;{{ product.title }}
            &lt;span class="float-end"&gt;$ {{ product.price }}&lt;/span&gt;
          &lt;/h6&gt;
          &lt;a href="#" class="btn btn-outline-primary w-100" @click.prevent="addToCart(product.id)"&gt;加入購物車&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;`,
  computed: {
    ...mapState(productsStore, &#91;'sortProducts'])
  },
  methods: {
    ...mapActions(cartStore, &#91;'addToCart'])
  },
}
</code></pre>



<h3 class="wp-block-heading">08. 購物車資訊 Store</h3>



<ul class="wp-block-list">
<li>使用簡報解釋頁面元件結構</li>



<li>修改 cartStore.js 檔案</li>



<li>修改 cartComponent.js 檔案，匯入 cartStore、使用 mapState</li>



<li>修改 cartStore.js 檔案，調整 cartList</li>
</ul>



<pre class="wp-block-code"><code>// pinia/store/cartStore.js
const { defineStore } = Pinia;
import productsStore from './productsStore.js';

export default defineStore('cart', {
  // methods
  // actions
  state: () =&gt; ({
    cart: &#91;]
  }),
  actions: {
    addToCart(productId, qty = 1) {
      console.log(productId, qty);
      this.cart.push({
        id: new Date().getTime(),
        productId,
        qty
      })
      // console.log(this.cart);
    }
  },
  getters: {
    cartList: ({ cart }) =&gt; {
      // 1. 購物車的品項資訊，需要整合產品資訊
      // 2. 必須計算小計的金額
      // 3. 必須提供總金額
      const { products } = productsStore();
      // console.log(products);
      // console.log(cart);
      const carts = cart.map((item) =&gt; {
        // console.log(item);
        // 單一產品取出
        const product = products.find((product) =&gt; product.id === item.productId);
        // console.log('相同 id 的產品', product);
        return {
          ...item,
          product,
          subtotal: product.price * item.qty
        }
      })
      // console.log(carts);
      const total = carts.reduce((a, b) =&gt; a + b.subtotal ,0);
      // console.log(total);

      return {
        carts, // 列表
        total
      }
    }
  }
})
</code></pre>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/cartComponent.js
import cartStore from '../store/cartStore.js'
const { mapState } = Pinia;

export default {
  template: `&lt;div class="bg-light my-4 p-4"&gt;
    &lt;div&gt;購物車沒有任何品項&lt;/div&gt; &lt;!-- v-if --&gt;
    &lt;!-- v-else --&gt;
    &lt;table class="table align-middle"&gt;
    &lt;tbody&gt;
      &lt;tr v-for="item in cartList.carts"&gt;
        &lt;td&gt;
          &lt;a href="#" class="text-dark"&gt;x&lt;/a&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;img src="https://images.unsplash.com/photo-1597733153203-a54d0fbc47de?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1090&amp;q=80" class="table-image" alt=""&gt;
        &lt;/td&gt;
        &lt;td&gt;好吃的餅乾&lt;/td&gt;
        &lt;td&gt;
          &lt;select name="" id="" class="form-select"&gt;
            &lt;option value=""&gt;1&lt;/option&gt;
          &lt;/select&gt;
        &lt;/td&gt;
        &lt;td class="text-end"&gt;
          $900
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
    &lt;tfoot&gt;
      &lt;td colspan="5" class="text-end"&gt;總金額 NT$ {{cartList.total}}&lt;/td&gt;
    &lt;/tfoot&gt;
    &lt;/table&gt;
  &lt;/div&gt;`,
  computed: {
    ...mapState(cartStore, &#91;'cartList'])
  }
}
</code></pre>



<h3 class="wp-block-heading">09. 呈現購物車列表並刪除品項</h3>



<ul class="wp-block-list">
<li>修改 cartComponent.js 檔案</li>



<li>修改 cartStore.js 檔案，撰寫刪除的方法</li>
</ul>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/cartComponent.js
import cartStore from '../store/cartStore.js'
const { mapState, mapActions } = Pinia;

export default {
  template: `&lt;div class="bg-light my-4 p-4"&gt;
    &lt;div v-if="!cartList.carts.length"&gt;購物車沒有任何品項&lt;/div&gt; &lt;!-- v-if --&gt;
    &lt;table v-else class="table align-middle"&gt;
    &lt;tbody&gt;
      &lt;tr v-for="item in cartList.carts" :key="item.id"&gt;
        &lt;td width="100"&gt;
          &lt;a href="#" class="text-dark"
          @click.prevent="removeCartItem(item.id)"&gt;x&lt;/a&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;img :src="item.product.imageUrl" class="table-image" alt=""&gt;
        &lt;/td&gt;
        &lt;td&gt;{{ item.product.title }}&lt;/td&gt;
        &lt;td&gt;
          &lt;select name="" id="" class="form-select"&gt;
            &lt;option value=""&gt;1&lt;/option&gt;
          &lt;/select&gt;
        &lt;/td&gt;
        &lt;td class="text-end"&gt;
          $ {{ item.subtotal }}
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
    &lt;tfoot&gt;
      &lt;td colspan="5" class="text-end"&gt;總金額 NT$ {{cartList.total}}&lt;/td&gt;
    &lt;/tfoot&gt;
    &lt;/table&gt;
  &lt;/div&gt;`,
  computed: {
    ...mapState(cartStore, &#91;'cartList'])
  },
  methods: {
    ...mapActions(cartStore, &#91;'removeCartItem'])
  }
}
</code></pre>



<pre class="wp-block-code"><code>// pinia/store/cartStore.js
const { defineStore } = Pinia;
import productsStore from './productsStore.js';

export default defineStore('cart', {
  // methods
  // actions
  state: () =&gt; ({
    cart: &#91;]
  }),
  actions: {
    addToCart(productId, qty = 1) {
      console.log(productId, qty);
      this.cart.push({
        id: new Date().getTime(),
        productId,
        qty
      })
      // console.log(this.cart);
    },
    removeCartItem(id) {
      const index = this.cart.findIndex((item) =&gt; item.id === id);
      this.cart.splice(index, 1);
    }
  },
  getters: {
    cartList: ({ cart }) =&gt; {
      // 1. 購物車的品項資訊，需要整合產品資訊
      // 2. 必須計算小計的金額
      // 3. 必須提供總金額
      const { products } = productsStore();
      // console.log(products);
      // console.log(cart);
      const carts = cart.map((item) =&gt; {
        // console.log(item);
        // 單一產品取出
        const product = products.find((product) =&gt; product.id === item.productId);
        // console.log('相同 id 的產品', product);
        return {
          ...item,
          product,
          subtotal: product.price * item.qty
        }
      })
      // console.log(carts);
      const total = carts.reduce((a, b) =&gt; a + b.subtotal ,0);
      // console.log(total);

      return {
        carts, // 列表
        total
      }
    }
  }
})
</code></pre>



<h3 class="wp-block-heading">10. 新增品項加總至原品項</h3>



<ul class="wp-block-list">
<li>修改 cartStore.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// pinia/store/cartStore.js
const { defineStore } = Pinia;
import productsStore from './productsStore.js';

export default defineStore('cart', {
  // methods
  // actions
  state: () =&gt; ({
    cart: &#91;]
  }),
  actions: {
    addToCart(productId, qty = 1) {
      // 取得已經有加入購物車的項目
      // 進行判斷，如果購物車有該項目則 +1，如果沒有則是新增一個購物車項目
      const currentCart = this.cart.find((item) =&gt; item.productId === productId)

      if (currentCart) {
        currentCart.qty += qty;
      } else {
        this.cart.push({
          id: new Date().getTime(),
          productId,
          qty
        });
      }
      console.log(this.cart);
      // console.log(this.cart);
    },
    removeCartItem(id) {
      const index = this.cart.findIndex((item) =&gt; item.id === id);
      this.cart.splice(index, 1);
    }
  },
  getters: {
    cartList: ({ cart }) =&gt; {
      // 1. 購物車的品項資訊，需要整合產品資訊
      // 2. 必須計算小計的金額
      // 3. 必須提供總金額
      const { products } = productsStore();
      // console.log(products);
      // console.log(cart);
      const carts = cart.map((item) =&gt; {
        // console.log(item);
        // 單一產品取出
        const product = products.find((product) =&gt; product.id === item.productId);
        // console.log('相同 id 的產品', product);
        return {
          ...item,
          product,
          subtotal: product.price * item.qty
        }
      })
      // console.log(carts);
      const total = carts.reduce((a, b) =&gt; a + b.subtotal ,0);
      // console.log(total);

      return {
        carts, // 列表
        total
      }
    }
  }
})
</code></pre>



<h3 class="wp-block-heading">11. 設定數量</h3>



<ul class="wp-block-list">
<li>修改 cartComponent.js 檔案</li>



<li>修改 cartStore.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// pinia/homeworkComponent/cartComponent.js
import cartStore from '../store/cartStore.js'
const { mapState, mapActions } = Pinia;

export default {
  template: `&lt;div class="bg-light my-4 p-4"&gt;
    &lt;div v-if="!cartList.carts.length"&gt;購物車沒有任何品項&lt;/div&gt; &lt;!-- v-if --&gt;
    &lt;table v-else class="table align-middle"&gt;
    &lt;tbody&gt;
      &lt;tr v-for="item in cartList.carts" :key="item.id"&gt;
        &lt;td width="100"&gt;
          &lt;a href="#" class="text-dark"
          @click.prevent="removeCartItem(item.id)"&gt;x&lt;/a&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;img :src="item.product.imageUrl" class="table-image" alt=""&gt;
        &lt;/td&gt;
        &lt;td&gt;{{ item.product.title }}&lt;/td&gt;
        &lt;td&gt;
          &lt;select name="" id="" class="form-select" :value="item.qty"
          @change="(evt) =&gt; setCartQty(item.id, evt)"&gt;
            &lt;option :value="i" v-for="i in 20" :key="i"&gt;{{ i }}&lt;/option&gt;
          &lt;/select&gt;
        &lt;/td&gt;
        &lt;td class="text-end"&gt;
          $ {{ item.subtotal }}
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
    &lt;tfoot&gt;
      &lt;td colspan="5" class="text-end"&gt;總金額 NT$ {{cartList.total}}&lt;/td&gt;
    &lt;/tfoot&gt;
    &lt;/table&gt;
  &lt;/div&gt;`,
  computed: {
    ...mapState(cartStore, &#91;'cartList'])
  },
  methods: {
    ...mapActions(cartStore, &#91;'removeCartItem', 'setCartQty'])
  }
}
</code></pre>



<pre class="wp-block-code"><code>// pinia/store/cartStore.js
const { defineStore } = Pinia;
import productsStore from './productsStore.js';

export default defineStore('cart', {
  // methods
  // actions
  state: () =&gt; ({
    cart: &#91;]
  }),
  actions: {
    addToCart(productId, qty = 1) {
      // 取得已經有加入購物車的項目
      // 進行判斷，如果購物車有該項目則 +1，如果沒有則是新增一個購物車項目
      const currentCart = this.cart.find((item) =&gt; item.productId === productId)

      if (currentCart) {
        currentCart.qty += qty;
      } else {
        this.cart.push({
          id: new Date().getTime(),
          productId,
          qty
        });
      }
      console.log(this.cart);
      // console.log(this.cart);
    },
    setCartQty(id, event) {
      // console.log(id, event);
      // console.log(event.target.value, typeof event.target.value);
      const currentCart = this.cart.find((item) =&gt; item.id === id);
      // console.log(currentCart);
      currentCart.qty = event.target.value * 1;
    },
    removeCartItem(id) {
      const index = this.cart.findIndex((item) =&gt; item.id === id);
      this.cart.splice(index, 1);
    }
  },
  getters: {
    cartList: ({ cart }) =&gt; {
      // 1. 購物車的品項資訊，需要整合產品資訊
      // 2. 必須計算小計的金額
      // 3. 必須提供總金額
      const { products } = productsStore();
      // console.log(products);
      // console.log(cart);
      const carts = cart.map((item) =&gt; {
        // console.log(item);
        // 單一產品取出
        const product = products.find((product) =&gt; product.id === item.productId);
        // console.log('相同 id 的產品', product);
        return {
          ...item,
          product,
          subtotal: product.price * item.qty
        }
      })
      // console.log(carts);
      const total = carts.reduce((a, b) =&gt; a + b.subtotal ,0);
      // console.log(total);

      return {
        carts, // 列表
        total
      }
    }
  }
})
</code></pre>



<h3 class="wp-block-heading">12. Navbar 數量呈現</h3>



<ul class="wp-block-list">
<li>修改 navbarComponent.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/navbarComponent.js
const { mapState } = Pinia;
import cartStore from "../store/cartStore.js";

export default {
  template: `&lt;nav class="navbar bg-body-tertiary"&gt;
    &lt;div class="container-fluid"&gt;
      &lt;span class="navbar-brand mb-0 h1"&gt;香香餅乾店&lt;/span&gt;
      &lt;button type="button" class="btn"&gt;購物車
        &lt;span class="badge rounded-pill bg-danger text-white"&gt;{{ cart.length }}&lt;/span&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/nav&gt;`,
  computed: {
    ...mapState(cartStore, &#91;'cart'])
  }
}</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Vue3 複習09</title>
		<link>/wordpress_blog/reviewvue3_09/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Tue, 23 Apr 2024 09:06:43 +0000</pubDate>
				<category><![CDATA[六角學院]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=837</guid>

					<description><![CDATA[最終挑戰 最終作業課程介紹 課程目標 透過課程專屬 API 完成 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">最終挑戰</h2>



<h3 class="wp-block-heading">最終作業課程介紹</h3>



<h4 class="wp-block-heading">課程目標</h4>



<p>透過課程專屬 API 完成獨立作品</p>



<h4 class="wp-block-heading">電商網站操作流程</h4>



<ul class="wp-block-list">
<li>管理員</li>



<li>用戶</li>
</ul>



<h4 class="wp-block-heading">開發的架構說明</h4>



<ul class="wp-block-list">
<li>前端
<ul class="wp-block-list">
<li>前台</li>



<li>後台</li>
</ul>
</li>



<li>後端</li>
</ul>



<h4 class="wp-block-heading">學習面向</h4>



<ul class="wp-block-list">
<li>後台 – 登入授權、編輯商品、檢視訂單、建立優惠券…<br>學習開發流程、後台邏輯建構</li>



<li>前台 – 檢視商品、選購並加入購物車、提交訂單、模擬付款…
<ul class="wp-block-list">
<li>完整作品經營</li>



<li>美感、細節培養</li>



<li>解決問題的能力</li>
</ul>
</li>
</ul>



<h4 class="wp-block-heading">API 說明</h4>



<p>[API]: https://站點/api/:api_path/admin/product</p>



<ul class="wp-block-list">
<li>申請帳號 – 登入授權也是使用此帳號</li>



<li>申請專屬 API 連結 – 資料內容也會獨立</li>



<li>開始練習</li>
</ul>



<h4 class="wp-block-heading">API 文件</h4>



<p>[API]: https://站點/api/:api_path/admin/product</p>



<p>[方法]: POST</p>



<pre class="wp-block-code"><code>// &#91;參數]:
{
  "data": {
    "title": "&#91;賣]動物園造型衣服3",
    "category": "衣服2",
    ...
  }
}
</code></pre>



<h3 class="wp-block-heading">課程 API 文件及相關資源</h3>



<ol class="wp-block-list">
<li>課程需要先註冊屬於個人的 API 路徑，註冊方法在下一小節會介紹，<a href="https://vue3-course-api.hexschool.io/" target="_blank" rel="noreferrer noopener">註冊網址與 API 站點連結</a></li>



<li><a href="https://github.com/hexschool/vue3-course-api-wiki/wiki" target="_blank" rel="noreferrer noopener">API 文件</a></li>



<li>課程中後期，不會所有步驟都一一說明，所以課程中有提供每個階段的 commit，讓大家可以看到每個章節老師修改了哪些部分：<a href="https://github.com/hexschool/vue3-course-api-wiki/wiki/%E9%80%B2%E5%BA%A6-Commit" target="_blank" rel="noreferrer noopener">所有課程進度 Commit (對應課程章節)</a></li>



<li>課程中也會提供許多 HTML 片段模板，減少重複繁瑣的行為，如提到會提供模板的部分，<a href="https://github.com/hexschool/vue3-course-api-wiki/wiki/%E8%AA%B2%E7%A8%8B%E9%83%A8%E5%88%86%E6%A8%A1%E6%9D%BF" target="_blank" rel="noreferrer noopener">連結</a></li>



<li>雖然課程中 ESLint 選擇為 Airbnb 格式：
<ul class="wp-block-list">
<li>體驗簡單一點的開發規則可選擇 Standard</li>



<li>對 ES6 及錯誤排除有一定掌握者可選擇 Airbnb</li>
</ul>
</li>
</ol>



<p>關於 ESLint 搭配 VSCode 的自動排版可參考（注意，並非所有錯誤都可自動排除）：<a href="https://wcc723.github.io/development/2021/04/11/vscode-eslint-prettier/" target="_blank" rel="noreferrer noopener">連結</a></p>



<p>使用時 ESLint 時：</p>



<ul class="wp-block-list">
<li>可多利用文字編輯器的提示來除錯（除錯也是開發者必學的技能之一）</li>



<li>盡可能避免關閉 ESLint 的提示</li>
</ul>



<h3 class="wp-block-heading">申請課程 API</h3>



<h4 class="wp-block-heading">流程說明:</h4>



<ol class="wp-block-list">
<li>申請一個專屬的課程練習帳號</li>



<li>登入帳號，並申請一個 API 路徑</li>



<li>測試 API 是否可以運作，並且開始實作</li>
</ol>



<h4 class="wp-block-heading">複習 Vue Cli 建立環境</h4>



<ol class="wp-block-list">
<li>使用 Vue Cli 建立環境 vue create vue3_dashboard_record</li>



<li>Please pick a preset: Manually select features</li>



<li>Check the features needed for your project: Choose Vue version, Babel, Router, CSS Pre-processors, Linter</li>



<li>Choose a version of Vue.js that you want to start the project with 3.x (Preview)</li>



<li>Use history mode for router? (Requires proper server setup for index fallback in production) No</li>



<li>Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)</li>



<li>Pick a linter / formatter config: Airbnb</li>



<li>Pick additional lint features: Lint on save</li>



<li>Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files</li>



<li>Save this as a preset for future projects? (y/N) N</li>
</ol>



<h4 class="wp-block-heading">API 路徑加到環境變數</h4>



<ol class="wp-block-list">
<li>在根目錄新增 .env 檔案<br>API Server 路徑: VUE_APP_API<br>API 個人路徑: VUE_APP_PATH</li>



<li>開啟 Home.vue 檔案調整程式碼內容<br>使用生命週期讀出環境變數</li>



<li>重新執行 npm run serve</li>



<li>開啟 Console 查看環境變數是否有顯示</li>
</ol>



<pre class="wp-block-code"><code>// .env
VUE_APP_API=http://localhost:3000/
VUE_APP_PATH=geehsu-api
</code></pre>



<pre class="wp-block-code"><code>// views/Home.vue
&lt;template&gt;
  &lt;div class="home"&gt;
    &lt;img alt="Vue logo" src="../assets/logo.png"&gt;
    &lt;HelloWorld msg="Welcome to Your Vue.js App"/&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue';

export default {
  name: 'Home',
  components: {
    HelloWorld,
  },
  created() {
    console.log(process.env.VUE_APP_API, process.env.VUE_APP_PATH);
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// 除錯 Console 警告
// vue.config.js
module.exports = {
  chainWebpack: (config) =&gt; {
    config.plugin('define').tap((definitions) =&gt; {
      Object.assign(definitions&#91;0], {
        __VUE_OPTIONS_API__: 'true',
        __VUE_PROD_DEVTOOLS__: 'false',
        __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false'
      })
      return definitions
    })
  }
}
</code></pre>



<h3 class="wp-block-heading">常見 API 問題解決方式</h3>



<h4 class="wp-block-heading">路徑與方法是一對的</h4>



<p>API 是由路徑 + 方法組成的，文件中皆會標示所有 API 可用的方法如 GET、POST、DELETE 等等，必須使用完全對應的方法才能運作喔。</p>



<h4 class="wp-block-heading">從錯誤訊息中尋找問題</h4>



<p>API 發送的過程中錯一個字就會無法運行，盡可能從錯誤的回饋中尋找問題，檢查：</p>



<ul class="wp-block-list">
<li>路徑是否拼對</li>



<li>GET、POST 等方法是否正確</li>



<li>程式碼運作是否如預期</li>
</ul>



<h4 class="wp-block-heading">遇到錯誤無法解決</h4>



<p>發問流程:<br><strong>可以提供完整程式碼讓老師、助教測試</strong><br>可以將原始碼上傳至 GitHub 或雲端硬碟，並將說明及連結提供在問答區<br>上傳時特別注意: 不需要夾帶 node_modules、dist 資料夾<br>這會導致信件無法開啟，且下載後依然要刪除重新安裝。</p>



<p>另外請<strong>盡可能提供完整訊息</strong>，由於許多問題並非片段就能判斷<br>所以盡可能提供越完整越好</p>



<ul class="wp-block-list">
<li>錯誤的問題描述 (哪一個 API、做了什麼事、預期有怎樣的進展、錯誤的問題點)</li>



<li>錯誤的 API 連結為何</li>



<li>完整錯誤的訊息圖片 (Chrome Console)</li>



<li>出現錯誤的程式碼</li>
</ul>



<h3 class="wp-block-heading">匯入 Bootstrap 並調整版型</h3>



<ol class="wp-block-list">
<li>安裝 Bootstrap 套件 – npm install bootstrap</li>



<li>引用 Bootstrap 到專案裡面<br>Bootstrap 文件 &gt; Customize &gt; Sass &gt; Importing<br>複製 @import “../node_modules/bootstrap/scss/bootstrap”;</li>



<li>開啟 App.vue 檔案貼上程式碼後改寫<br>@import “~bootstrap/scss/bootstrap”;</li>



<li>重新運行 npm run serve</li>



<li>在 Bootstrap 文件 &gt; 元件 &gt; 按鈕<br>複製按鈕程式碼貼到 App.vue 檔案測試是否有正確載入 Bootstrap 套件</li>



<li>客製化 Bootstrap 樣式<br>在 assets 資料夾新增 all.scss 檔案<br>在 assets 資料夾新增 helpers 資料夾<br>在 assets/helpers 新增 _variables.scss 檔案 (_在scss不會被編譯出來)</li>



<li>找到 node_modules/bootstrap/scss/_variables.scss 打開<br>複製 _variables.scss 所有程式碼直接貼到 assets/helpers/_variables.scss 檔案</li>



<li>客製化變數<br>在 Bootstrap 文件 &gt; Customize &gt; Sass &gt; Importing<br>複製文件需要的程式碼貼到 assets/all.scss 並改寫<br>匯入所有的 Bootstrap</li>



<li>在 App.vue 檔案調整成自定義 Sass 匯入路徑</li>



<li>在 assets/helpers/_variables.scss 檔案調整變數</li>
</ol>



<pre class="wp-block-code"><code>// App.vue
&lt;template&gt;
  &lt;div id="nav"&gt;
    &lt;router-link to="/"&gt;Home&lt;/router-link&gt; |
    &lt;router-link to="/about"&gt;About&lt;/router-link&gt;
  &lt;/div&gt;
  &lt;button type="button" class="btn btn-primary"&gt;Primary&lt;/button&gt;
  &lt;button type="button" class="btn btn-secondary"&gt;Secondary&lt;/button&gt;
  &lt;button type="button" class="btn btn-success"&gt;Success&lt;/button&gt;
  &lt;button type="button" class="btn btn-danger"&gt;Danger&lt;/button&gt;
  &lt;button type="button" class="btn btn-warning"&gt;Warning&lt;/button&gt;
  &lt;button type="button" class="btn btn-info"&gt;Info&lt;/button&gt;
  &lt;button type="button" class="btn btn-light"&gt;Light&lt;/button&gt;
  &lt;button type="button" class="btn btn-dark"&gt;Dark&lt;/button&gt;

  &lt;button type="button" class="btn btn-link"&gt;Link&lt;/button&gt;
  &lt;router-view/&gt;
&lt;/template&gt;

&lt;style lang="scss"&gt;
@import "./assets/all";
&lt;/style&gt;
</code></pre>



<pre class="wp-block-code"><code>// assets/helpers/_variables.scss
// scss-docs-start theme-colors-map
$theme-colors: (
  "primary":    black , // $primary,
  "secondary":  $secondary,
  "success":    $success,
  "info":       $info,
  "warning":    $warning,
  "danger":     $danger,
  "light":      $light,
  "dark":       $dark
) !default;
// scss-docs-end theme-colors-map</code></pre>



<pre class="wp-block-code"><code>// assets/all.scss
// 1. Include functions first (so you can manipulate colors, SVGs, calc, etc)
@import "~bootstrap/scss/functions";

// 2. Include any default variable overrides here

// 3. Include remainder of required Bootstrap stylesheets (including any separate color mode stylesheets)
// 客製化 variables 路徑
@import "./helpers/variables";
@import "~bootstrap/scss/variables-dark";

// 4. Include any default map overrides here

// 5. Include remainder of required parts
@import "~bootstrap/scss/maps";
@import "~bootstrap/scss/mixins";
@import "~bootstrap/scss/root";

// 匯入所有的 Bootstrap
@import "~bootstrap/scss/bootstrap";
</code></pre>



<h3 class="wp-block-heading">製作登入頁面</h3>



<p>注意事項：.env 檔中的站點需要調整才能夠對應到文件 範例如下：</p>



<pre class="wp-block-code"><code>VUE_APP_API=https://vue3-course-api.hexschool.io/
</code></pre>



<p><a rel="noreferrer noopener" href="https://github.com/hexschool/vue3-course-api-wiki/wiki/%E7%99%BB%E5%85%A5%E5%8F%8A%E9%A9%97%E8%AD%89" target="_blank">登入及驗證</a>、<a href="https://www.npmjs.com/package/vue-axios">vue-a</a><a rel="noreferrer noopener" href="https://www.npmjs.com/package/vue-axios" target="_blank">xio</a><a href="https://www.npmjs.com/package/vue-axios">s</a>、<a href="https://github.com/hexschool/vue3-course-api-wiki/wiki/%E8%AA%B2%E7%A8%8B%E9%83%A8%E5%88%86%E6%A8%A1%E6%9D%BF" target="_blank" rel="noreferrer noopener">課程部分模板</a></p>



<ol class="wp-block-list">
<li>查看登入及驗證 API 文件<br>[API]、[方法]、[參數]、[成功回應]、[失敗回應]</li>



<li>安裝 vue-axios 套件<br>npm install –save axios vue-axios</li>



<li>匯入 vue-axios 套件<br>Vue2, Vue3 載入方式有所不同<br>複製程式碼貼到 main.js 檔案</li>



<li>調整 main.js 檔案結構</li>



<li>重新運行 npm run serve</li>



<li>新增 views/Login.vue 檔案<br>製作簡單頁面確保現在頁面已經建立成功<br>建立檔案再調整路由</li>



<li>查看 login 畫面有無正確運作<br>http://localhost:8080/#/login</li>



<li>清除 App.vue 檔案多餘的程式碼片段</li>



<li>在 Login.vue 加入登入的版型<br>使用課程部分模板調整使用</li>



<li>撰寫 Login.vue &lt;script&gt; JS 的部分<br>查看登入及驗證文件登入參數的物件格式、撰寫在 data 資料<br>使用 v-model 雙向綁定表單資料</li>



<li>測試是否能正常運作<br>輸入表單資料送出、查看 Vue.js devtools</li>



<li>串接 API<br>在 Login.vue 撰寫 methods 方法<br>把事件加到 &lt;form&gt; 標籤，使用 @submit 事件<br>可加上 prevent 避免觸發 html 預設事件</li>



<li>串接 API 必須把路徑組起來<br>環境變數站點位置+登入的實際 API<br>${process.env.VUE_APP_API}/admin/signin</li>



<li>送出 api<br>使用 this.$http.post 方法</li>



<li>試著登入測試是否有回傳資料</li>
</ol>



<pre class="wp-block-code"><code>// src/main.js
import { createApp } from 'vue';
import axios from 'axios';
import VueAxios from 'vue-axios';
import App from './App.vue';
import router from './router';

const app = createApp(App);
app.use(VueAxios, axios);
app.use(router);
app.mount('#app');
</code></pre>



<pre class="wp-block-code"><code>// views/Login.vue
&lt;template&gt;
  &lt;div class="container mt-5"&gt;
    &lt;form class="row justify-content-center"
    @submit.prevent="signIn"&gt;
      &lt;div class="col-md-6"&gt;
        &lt;h1 class="h3 mb-3 font-weight-normal"&gt;請先登入&lt;/h1&gt;
        &lt;div class="mb-2"&gt;
          &lt;label for="inputEmail" class="sr-only"&gt;Email address&lt;/label&gt;
          &lt;input
            type="email"
            id="inputEmail"
            class="form-control"
            placeholder="Email address"
            required
            autofocus
            v-model="user.username"
          /&gt;
        &lt;/div&gt;
        &lt;div class="mb-2"&gt;
          &lt;label for="inputPassword" class="sr-only"&gt;Password&lt;/label&gt;
          &lt;input
            type="password"
            id="inputPassword"
            class="form-control"
            placeholder="Password"
            required
            v-model="user.password"
          /&gt;
        &lt;/div&gt;
        &lt;div class="text-end mt-4"&gt;
          &lt;button class="btn btn-lg btn-primary btn-block" type="submit"&gt;登入&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      user: {
        username: '',
        password: '',
      },
    };
  },
  methods: {
    signIn() {
      console.log('login');
      // 環境變數站點位置 + 登入的實際 API
      const api = `${process.env.VUE_APP_API}admin/signin`;
      console.log(api);
      // api 路徑, 夾帶的資料
      this.$http.post(api, this.user)
        .then((res) =&gt; {
          console.log(res);
        });
    },
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// router/index.js
import { createRouter, createWebHashHistory } from 'vue-router';
import Home from '../views/Home.vue';

const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/login',
    component: () =&gt; import('../views/Login.vue'),
  },
];

const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

export default router;
</code></pre>



<pre class="wp-block-code"><code>// App.vue
&lt;template&gt;
  &lt;div id="nav"&gt;
    &lt;router-link to="/"&gt;Home&lt;/router-link&gt; |
    &lt;router-link to="/about"&gt;About&lt;/router-link&gt;
  &lt;/div&gt;
  &lt;router-view/&gt;
&lt;/template&gt;

&lt;style lang="scss"&gt;
@import "./assets/all";
&lt;/style&gt;
</code></pre>



<pre class="wp-block-code"><code>// .env
VUE_APP_API=https://vue3-course-api.hexschool.io/
VUE_APP_PATH=geehsu-api
</code></pre>



<h3 class="wp-block-heading">登入與 Cookie</h3>



<ol class="wp-block-list">
<li>查看瀏覽器 Cookie 儲存位置</li>



<li>查看 MDN 文件 –&nbsp;<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Document/cookie#%E7%A4%BA%E4%BE%8B_3_%E5%8F%AA%E6%89%A7%E8%A1%8C%E6%9F%90%E4%BA%8B%E4%B8%80%E6%AC%A1" target="_blank" rel="noreferrer noopener">Cookie 文件連結</a></li>



<li>撰寫 document.cookie 程式碼</li>



<li>登入後查看瀏覽器 Cookie 是否有正確儲存</li>
</ol>



<pre class="wp-block-code"><code>// views/Login.vue
&lt;template&gt;
  &lt;div class="container mt-5"&gt;
    &lt;form class="row justify-content-center"
    @submit.prevent="signIn"&gt;
      &lt;div class="col-md-6"&gt;
        &lt;h1 class="h3 mb-3 font-weight-normal"&gt;請先登入&lt;/h1&gt;
        &lt;div class="mb-2"&gt;
          &lt;label for="inputEmail" class="sr-only"&gt;Email address&lt;/label&gt;
          &lt;input
            type="email"
            id="inputEmail"
            class="form-control"
            placeholder="Email address"
            required
            autofocus
            v-model="user.username"
          /&gt;
        &lt;/div&gt;
        &lt;div class="mb-2"&gt;
          &lt;label for="inputPassword" class="sr-only"&gt;Password&lt;/label&gt;
          &lt;input
            type="password"
            id="inputPassword"
            class="form-control"
            placeholder="Password"
            required
            v-model="user.password"
          /&gt;
        &lt;/div&gt;
        &lt;div class="text-end mt-4"&gt;
          &lt;button class="btn btn-lg btn-primary btn-block" type="submit"&gt;登入&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      user: {
        username: '',
        password: '',
      },
    };
  },
  methods: {
    signIn() {
      console.log('login');
      // 環境變數站點位置 + 登入的實際 API
      const api = `${process.env.VUE_APP_API}admin/signin`;
      console.log(api);
      // api 路徑, 夾帶的資料
      this.$http.post(api, this.user)
        .then((res) =&gt; {
          // Cookie 存取
          // 前面: 儲存 Cookie 內容; 後面: 到期日
          // 儲存 Cookie 內容: 自定義名稱 = token 值
          // 到期日轉成 token 可以儲存的編碼
          const { token, expired } = res.data;
          // console.log(token, expired);
          document.cookie = `hexToken=${token}; expires=${new Date(expired)}`;
          console.log(res);
        });
    },
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">Cookie 存取的語法</h3>



<p>MDN 文件，將 Cookie 存入、取出:&nbsp;<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Document/cookie" target="_blank" rel="noreferrer noopener">連結</a></p>



<pre class="wp-block-code"><code>// MDN 文件，將 Cookie 存入、取出 - 範例程式碼
const token = response.data.token;
const expired = response.data.expired;
console.log(token, expired);
document.cookie = `hexToken=${token};expires=${new Date(expired)};`;
</code></pre>



<p>Axios 文件，設定預設 Headers:&nbsp;<a href="https://github.com/axios/axios#global-axios-defaults" target="_blank" rel="noreferrer noopener">連結</a></p>



<pre class="wp-block-code"><code>// Axios 文件，設定預設 Headers - 範例程式碼
const token = document.cookie.replace(/(?:(?:^|.*;\s*)hexToken\s*=\s*(&#91;^;]*).*$)|^.*$/, '$1');
this.$http.defaults.headers.common.Authorization = `${token}`;
</code></pre>



<h3 class="wp-block-heading">確認是否維持登入狀態</h3>



<ol class="wp-block-list">
<li>查看登入及驗證文件<br>檢查用戶是否仍持續登入<br>驗證方法 – 將 Token 加入 Headers</li>



<li>Token<br>把 Token 從 Cookie 取出<br>把 Token 加到 Headers</li>



<li>查看 MDN 文件 –&nbsp;<a rel="noreferrer noopener" href="https://developer.mozilla.org/zh-CN/docs/Web/API/Document/cookie#%E7%A4%BA%E4%BE%8B_2_%E5%BE%97%E5%88%B0%E5%90%8D%E4%B8%BA_test2_%E7%9A%84_cookie" target="_blank">Cookie 文件連結</a><br>複製 myCookie 程式碼並做改寫</li>



<li>查看 axios 文件 –&nbsp;<a href="https://github.com/axios/axios?tab=readme-ov-file#global-axios-defaults" target="_blank" rel="noreferrer noopener">Global axios defaults</a></li>



<li>實作把 Cookie 取出來、以及把 Token 發送出去</li>



<li>新增 views/Dashboard.vue 檔案<br>在 router/index.js 檔案把 Dashboard 加到路由表<br>http://localhost:8080/#/dashboard<br>查看是否有正確運作</li>



<li>在 Dashboard.vue 檔案撰寫程式碼<br>專注在 JS 撰寫<br>取出 token<br>把token 夾帶到 headers 裡面<br>複製 Global axios defaults 程式碼</li>



<li>試著觸發剛剛的 API<br>複製 Login.vue signIn() 裡面的程式碼<br>把多餘的程式碼清掉<br>檢查用戶是否仍然持續登入，把 API 路徑改成 api/user/check</li>



<li>查看 console 回傳的 data 是否是成功</li>



<li>清除 Cookie 查看是否仍正確登入</li>



<li>在 Login.vue 檔案補上登入判斷，登入成功後會轉址到 Dashboard 頁面</li>



<li>在 Dashboard.vue 檔案補上登入判斷，登入失敗後會轉址到 Login 頁面</li>
</ol>



<pre class="wp-block-code"><code>// views/Dashboard.vue
&lt;template&gt;
  Dashboard
&lt;/template&gt;

&lt;script&gt;
export default {
  created() {
    // 取出 token
    // test2 改寫成 hexToken
    // Airbnb 規範把錯誤的 \ 去除
    const token = document.cookie.replace(/(?:(?:^|.*;\s*)hexToken\s*=\s*(&#91;^;]*).*$)|^.*$/, '$1');
    // console.log(token);
    // token 夾帶到 headers 裡面
    // &#91;"Authorization"] 改寫成 .Authorization
    this.$http.defaults.headers.common.Authorization = token;
    // 檢查用戶是否仍然持續登入
    const api = `${process.env.VUE_APP_API}api/user/check`;
    this.$http.post(api)
      .then((res) =&gt; {
        // console.log(res);
        // 登入判斷 - 登入失敗
        if (!res.data.success) {
          // 登入失敗後會轉址到 Login 頁面
          this.$router.push('/login');
        }
      });
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// router/index.js
import { createRouter, createWebHashHistory } from 'vue-router';
import Home from '../views/Home.vue';

const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/login',
    component: () =&gt; import('../views/Login.vue'),
  },
  {
    path: '/dashboard',
    component: () =&gt; import('../views/Dashboard.vue'),
  },
];

const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

export default router;
</code></pre>



<pre class="wp-block-code"><code>// views/Login.vue
&lt;template&gt;
  &lt;div class="container mt-5"&gt;
    &lt;form class="row justify-content-center"
    @submit.prevent="signIn"&gt;
      &lt;div class="col-md-6"&gt;
        &lt;h1 class="h3 mb-3 font-weight-normal"&gt;請先登入&lt;/h1&gt;
        &lt;div class="mb-2"&gt;
          &lt;label for="inputEmail" class="sr-only"&gt;Email address&lt;/label&gt;
          &lt;input
            type="email"
            id="inputEmail"
            class="form-control"
            placeholder="Email address"
            required
            autofocus
            v-model="user.username"
          /&gt;
        &lt;/div&gt;
        &lt;div class="mb-2"&gt;
          &lt;label for="inputPassword" class="sr-only"&gt;Password&lt;/label&gt;
          &lt;input
            type="password"
            id="inputPassword"
            class="form-control"
            placeholder="Password"
            required
            v-model="user.password"
          /&gt;
        &lt;/div&gt;
        &lt;div class="text-end mt-4"&gt;
          &lt;button class="btn btn-lg btn-primary btn-block" type="submit"&gt;登入&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      user: {
        username: '',
        password: '',
      },
    };
  },
  methods: {
    signIn() {
      // console.log('login');
      // 環境變數站點位置 + 登入的實際 API
      const api = `${process.env.VUE_APP_API}admin/signin`;
      // console.log(api);
      // api 路徑, 夾帶的資料
      this.$http.post(api, this.user)
        .then((res) =&gt; {
          // 登入判斷 - 成功狀態
          if (res.data.success) {
            // Cookie 存取
            // 前面: 儲存 Cookie 內容; 後面: 到期日
            // 儲存 Cookie 內容: 自定義名稱 = token 值
            // 到期日轉成 token 可以儲存的編碼
            const { token, expired } = res.data;
            // console.log(token, expired);
            document.cookie = `hexToken=${token}; expires=${new Date(expired)}`;
            // console.log(res);
            // 登入成功會轉址到 Dashboard 頁面
            this.$router.push('/dashboard');
          }
        });
    },
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">調整巢狀路由並且加入 Navbar</h3>



<ol class="wp-block-list">
<li>新增 Products.vue 檔案 – 產品頁面</li>



<li>在 Dashboard.vue 檔案新增 Navbar<br>在 Bootstrap 文件搜尋 Navbar 尋找相對簡單的程式碼複製使用</li>



<li>在 App.vue 檔案把預設的連結清除<br>複製 &lt;router-view&gt; 貼到 Dashboard.vue</li>



<li>打開 router/index.js 路由表<br>在 dashboard 路由新增 products 子路由<br>http://localhost:8080/#/dashboard/products</li>



<li>在 Login.vue 檔案調整程式碼<br>登入成功後的轉址改為 /dashboard/products</li>



<li>在 Dashboard.vue 檔案把 Navbar 程式碼拆分到元件<br>在 src/components 新增 Navbar.vue 檔案<br>把 Dashboard.vue 檔案 Navbar 程式碼剪下貼到 Navbar.vue 檔案</li>



<li>在 Dashboard.vue 匯入 Navbar.vue<br>import Navbar from ‘../components/Navbar.vue’;<br>使用 components 區域註冊 Navbar<br>再把 &lt;Navbar&gt; 標籤加到畫面</li>



<li>在 Navbar.vue 新增登出的功能<br>查看登入及驗證文件<br>從 Login.vue 檔案查看 signIn 方法<br>撰寫 Navbar.vue logout 方法</li>
</ol>



<pre class="wp-block-code"><code>// views/Products.vue
&lt;template&gt;
  產品列表
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// views/Dashboard.vue
&lt;template&gt;
  &lt;Navbar&gt;&lt;/Navbar&gt;
  &lt;router-view/&gt;
&lt;/template&gt;

&lt;script&gt;
import Navbar from '../components/Navbar.vue';

export default {
  components: {
    Navbar,
  },
  created() {
    // 取出 token
    // test2 改寫成 hexToken
    // Airbnb 規範把錯誤的 \ 去除
    const token = document.cookie.replace(/(?:(?:^|.*;\s*)hexToken\s*=\s*(&#91;^;]*).*$)|^.*$/, '$1');
    // console.log(token);
    // token 夾帶到 headers 裡面
    // &#91;"Authorization"] 改寫成 .Authorization
    this.$http.defaults.headers.common.Authorization = token;
    // 檢查用戶是否仍然持續登入
    const api = `${process.env.VUE_APP_API}api/user/check`;
    this.$http.post(api)
      .then((res) =&gt; {
        // console.log(res);
        // 登入判斷 - 登入失敗
        if (!res.data.success) {
          // 登入失敗後會轉址到 Login 頁面
          this.$router.push('/login');
        }
      });
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// App.vue
&lt;template&gt;
  &lt;router-view/&gt;
&lt;/template&gt;

&lt;style lang="scss"&gt;
@import "./assets/all";
&lt;/style&gt;
</code></pre>



<pre class="wp-block-code"><code>// router/index.js
import { createRouter, createWebHashHistory } from 'vue-router';
import Home from '../views/Home.vue';

const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/login',
    component: () =&gt; import('../views/Login.vue'),
  },
  {
    path: '/dashboard',
    component: () =&gt; import('../views/Dashboard.vue'),
    // 巢狀路由 - 產品列表
    children: &#91;
      {
        path: 'products',
        component: () =&gt; import('../views/Products.vue'),
      },
    ],
  },
];

const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

export default router;
</code></pre>



<pre class="wp-block-code"><code>// Login.vue
&lt;template&gt;
  &lt;div class="container mt-5"&gt;
    &lt;form class="row justify-content-center"
    @submit.prevent="signIn"&gt;
      &lt;div class="col-md-6"&gt;
        &lt;h1 class="h3 mb-3 font-weight-normal"&gt;請先登入&lt;/h1&gt;
        &lt;div class="mb-2"&gt;
          &lt;label for="inputEmail" class="sr-only"&gt;Email address&lt;/label&gt;
          &lt;input
            type="email"
            id="inputEmail"
            class="form-control"
            placeholder="Email address"
            required
            autofocus
            v-model="user.username"
          /&gt;
        &lt;/div&gt;
        &lt;div class="mb-2"&gt;
          &lt;label for="inputPassword" class="sr-only"&gt;Password&lt;/label&gt;
          &lt;input
            type="password"
            id="inputPassword"
            class="form-control"
            placeholder="Password"
            required
            v-model="user.password"
          /&gt;
        &lt;/div&gt;
        &lt;div class="text-end mt-4"&gt;
          &lt;button class="btn btn-lg btn-primary btn-block" type="submit"&gt;登入&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      user: {
        username: '',
        password: '',
      },
    };
  },
  methods: {
    signIn() {
      // console.log('login');
      // 環境變數站點位置 + 登入的實際 API
      const api = `${process.env.VUE_APP_API}admin/signin`;
      // console.log(api);
      // api 路徑, 夾帶的資料
      this.$http.post(api, this.user)
        .then((res) =&gt; {
          // 登入判斷 - 成功狀態
          if (res.data.success) {
            // Cookie 存取
            // 前面: 儲存 Cookie 內容; 後面: 到期日
            // 儲存 Cookie 內容: 自定義名稱 = token 值
            // 到期日轉成 token 可以儲存的編碼
            const { token, expired } = res.data;
            // console.log(token, expired);
            document.cookie = `hexToken=${token}; expires=${new Date(expired)}`;
            // console.log(res);
            // 登入成功會轉址到 Dashboard 頁面
            // 改為 /dashboard/products
            this.$router.push('/dashboard/products');
          }
        });
    },
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/components/Navbar.vue
&lt;template&gt;
  &lt;nav class="navbar navbar-expand-lg bg-body-tertiary"&gt;
    &lt;div class="container-fluid"&gt;
      &lt;a class="navbar-brand" href="#"&gt;Navbar w/ text&lt;/a&gt;
      &lt;button class="navbar-toggler" type="button"
      data-bs-toggle="collapse" data-bs-target="#navbarText"
      aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation"&gt;
        &lt;span class="navbar-toggler-icon"&gt;&lt;/span&gt;
      &lt;/button&gt;
      &lt;div class="collapse navbar-collapse" id="navbarText"&gt;
        &lt;ul class="navbar-nav me-auto mb-2 mb-lg-0"&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link active" aria-current="page" href="#"&gt;Home&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#"&gt;Features&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#" @click.prevent="logout"&gt;登出&lt;/a&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
        &lt;span class="navbar-text"&gt;
          Navbar text with an inline element
        &lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  methods: {
    logout() {
      const api = `${process.env.VUE_APP_API}logout`;
      this.$http.post(api, this.user)
        .then((res) =&gt; {
          if (res.data.success) {
            // console.log(res);
            this.$router.push('/login');
          }
        });
    },
  },
};
&lt;/script&gt;
</code></pre>



<h4 class="wp-block-heading">導航守衛</h4>



<p>防止使用者觀看到後台介面。</p>



<p>參考: Day17：<a href="https://ithelp.ithome.com.tw/articles/10206041" target="_blank" rel="noreferrer noopener">如何防止使用者未登錄就要訪問頁面？</a></p>



<pre class="wp-block-code"><code>// router/index.js
import { createRouter, createWebHashHistory } from 'vue-router';
import axios from 'axios';
import Home from '../views/Home.vue';

const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/login',
    name: 'Login',
    component: () =&gt; import('../views/Login.vue'),
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () =&gt; import('../views/Dashboard.vue'),
    meta: { requiresAuth: true },
    children: &#91;
      {
        path: 'products',
        name: 'Products',
        component: () =&gt; import('../views/Products.vue'),
        meta: { requiresAuth: true },
      },
    ],
  },
];

const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

router.beforeEach((to, from, next) =&gt; {
  console.log('導航守衛啟動');
  if (to.meta.requiresAuth) {
    console.log('need auth');
    const token = document.cookie.replace(/(?:(?:^|.*;\s*)hexToken\s*=\s*(&#91;^;]*).*$)|^.*$/, '$1');
    axios.defaults.headers.common.Authorization = token;
    const api = `${process.env.VUE_APP_API}api/user/check`;
    axios.post(api).then((res) =&gt; {
      console.log(res.data);
      console.log(res.data.success);
      if (res.data.success) {
        next();
      } else {
        next({
          path: '/login',
        });
      }
    });
  } else next();
});

export default router;
</code></pre>



<p>參考:&nbsp;<a href="https://www.youtube.com/watch?v=wdIXW-j-99Y" target="_blank" rel="noreferrer noopener">Vue Router Guard</a></p>



<pre class="wp-block-code"><code>// router/index.js
import { createRouter, createWebHashHistory } from 'vue-router';
import Home from '../views/Home.vue';

const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/login',
    name: 'Login',
    component: () =&gt; import('../views/Login.vue'),
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () =&gt; import('../views/Dashboard.vue'),
    meta: { requiresAuth: true },
    children: &#91;
      {
        path: 'products',
        name: 'Products',
        component: () =&gt; import('../views/Products.vue'),
        meta: { requiresAuth: true },
      },
    ],
  },
];

const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

document.isAuthenticated = false;

router.beforeEach((to, from, next) =&gt; {
  if (to.meta.requiresAuth &amp;&amp; !document.isAuthenticated) next({ name: 'Login' });
  else next();
});

export default router;
</code></pre>



<pre class="wp-block-code"><code>// views/Login.vue
&lt;template&gt;
  &lt;div class="container mt-5"&gt;
    &lt;form class="row justify-content-center"
    @submit.prevent="signIn"&gt;
      &lt;div class="col-md-6"&gt;
        &lt;h1 class="h3 mb-3 font-weight-normal"&gt;請先登入&lt;/h1&gt;
        &lt;div class="mb-2"&gt;
          &lt;label for="inputEmail" class="sr-only"&gt;Email address&lt;/label&gt;
          &lt;input
            type="email"
            id="inputEmail"
            class="form-control"
            placeholder="Email address"
            required
            autofocus
            v-model="user.username"
          /&gt;
        &lt;/div&gt;
        &lt;div class="mb-2"&gt;
          &lt;label for="inputPassword" class="sr-only"&gt;Password&lt;/label&gt;
          &lt;input
            type="password"
            id="inputPassword"
            class="form-control"
            placeholder="Password"
            required
            v-model="user.password"
          /&gt;
        &lt;/div&gt;
        &lt;div class="text-end mt-4"&gt;
          &lt;button class="btn btn-lg btn-primary btn-block" type="submit"&gt;登入&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      user: {
        username: '',
        password: '',
      },
    };
  },
  methods: {
    signIn() {
      // console.log('login');
      // 環境變數站點位置 + 登入的實際 API
      const api = `${process.env.VUE_APP_API}admin/signin`;
      // console.log(api);
      // api 路徑, 夾帶的資料
      this.$http.post(api, this.user)
        .then((res) =&gt; {
          // 登入判斷 - 成功狀態
          if (res.data.success) {
            // Cookie 存取
            // 前面: 儲存 Cookie 內容; 後面: 到期日
            // 儲存 Cookie 內容: 自定義名稱 = token 值
            // 到期日轉成 token 可以儲存的編碼
            const { token, expired } = res.data;
            // console.log(token, expired);
            document.cookie = `hexToken=${token}; expires=${new Date(expired)}`;
            // console.log(res);
            // 登入成功會轉址到 Dashboard 頁面
            document.isAuthenticated = true;
            this.$router.push('/dashboard/products');
          }
        });
    },
  },
};
&lt;/script&gt;
</code></pre>



<p>建立 NotFound 頁面，然後轉址到首頁。</p>



<pre class="wp-block-code"><code>// views/NotFound.vue
&lt;template&gt;
  Not Found
&lt;/template&gt;

&lt;script&gt;
export default {
  created() {
    setTimeout(() =&gt; this.$router.push({
      path: '/',
    }), 5000);
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// router/index.js
import { createRouter, createWebHashHistory } from 'vue-router';
import Home from '../views/Home.vue';

const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/login',
    name: 'Login',
    component: () =&gt; import('../views/Login.vue'),
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () =&gt; import('../views/Dashboard.vue'),
    meta: { requiresAuth: true },
    children: &#91;
      {
        path: 'products',
        name: 'Products',
        component: () =&gt; import('../views/Products.vue'),
        meta: { requiresAuth: true },
      },
    ],
  },
  // 404 頁面
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () =&gt; import('../views/NotFound.vue'),
  },
];

const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

document.isAuthenticated = false;

router.beforeEach((to, from, next) =&gt; {
  if (to.meta.requiresAuth &amp;&amp; !document.isAuthenticated) next({ name: 'Login' });
  else next();
});

export default router;
</code></pre>



<h3 class="wp-block-heading">加入產品列表</h3>



<ol class="wp-block-list">
<li>主要撰寫 Products.vue 檔案<br>稍微調整 Dashboard.vue 檔案</li>



<li>調整 Dashboard.vue 檔案程式碼<br>&lt;router-view&gt; 標籤外層加上 .container-fluid</li>



<li>在 Products.vue 檔案<br>用列表的形式完成，複製課程部分模板程式碼貼上</li>



<li>在 Products.vue 撰寫 JS 部分<br>定義 data() 回傳資料，products 資料是陣列、分頁資訊是物件</li>



<li>在 Products.vue 撰寫取得遠端資料的方法<br>定義 methods 物件，getProducts() 方法<br>getProducts() 取得產品列表是多數的產品資訊</li>



<li>查看 API 文件 – 管理控制台 [需驗證]<br>找到取得商品列表，透過 API 路徑取得遠端資料<br>參考 Login.vue 檔案 signIn() 方法調整成取得商品列表的 API 結構<br>[API]: /api/:api_path/admin/products?page=:page<br>尚未製作分頁可以先把分頁後面的 API 移除<br>把 API 和 PATH 改寫成環境變數的路徑<br>${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/products<br>API 方法是使用 get</li>



<li>使用生命週期 created() 觸發 getProducts() 方法<br>查看 Console 是否有正確取得遠端資料<br>可以儲存產品資訊和分頁資訊，調整 getProducts() 方法程式碼</li>



<li>使用 Vue 開發者工具檢視</li>



<li>把產品資訊渲染到畫面上</li>
</ol>



<pre class="wp-block-code"><code>// views/Products.vue
&lt;template&gt;
  &lt;table class="table mt-4"&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th width="120"&gt;分類&lt;/th&gt;
        &lt;th&gt;產品名稱&lt;/th&gt;
        &lt;th width="120"&gt;原價&lt;/th&gt;
        &lt;th width="120"&gt;售價&lt;/th&gt;
        &lt;th width="100"&gt;是否啟用&lt;/th&gt;
        &lt;th width="200"&gt;編輯&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr v-for="item in products" :key="item.id"&gt;
        &lt;td&gt;{{ item.category }}&lt;/td&gt;
        &lt;td&gt;{{ item.title }}&lt;/td&gt;
        &lt;td class="text-right"&gt;
          {{ item.origin_price }}
        &lt;/td&gt;
        &lt;td class="text-right"&gt;
          {{ item.price }}
        &lt;/td&gt;
        &lt;td&gt;
          &lt;span class="text-success" v-if="item.is_enabled"&gt;啟用&lt;/span&gt;
          &lt;span class="text-muted" v-else&gt;未啟用&lt;/span&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;div class="btn-group"&gt;
            &lt;button class="btn btn-outline-primary btn-sm"&gt;編輯&lt;/button&gt;
            &lt;button class="btn btn-outline-danger btn-sm"&gt;刪除&lt;/button&gt;
          &lt;/div&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      // 產品資訊
      products: &#91;],
      // 分頁資訊
      pagination: {},
    };
  },
  methods: {
    // 取得產品列表是多數的產品資訊
    getProducts() {
      const api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/products`;
      this.$http.get(api)
        .then((res) =&gt; {
          if (res.data.success) {
            console.log(res.data);
            this.products = res.data.products;
            this.pagination = res.data.pagination;
          }
        });
    },
  },
  created() {
    this.getProducts();
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// views/Dashboard.vue
&lt;template&gt;
  &lt;Navbar&gt;&lt;/Navbar&gt;
  &lt;div class="container-fluid"&gt;
    &lt;router-view/&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import Navbar from '../components/Navbar.vue';

export default {
  components: {
    Navbar,
  },
  created() {
    // 取出 token
    // test2 改寫成 hexToken
    // Airbnb 規範把錯誤的 \ 去除
    const token = document.cookie.replace(/(?:(?:^|.*;\s*)hexToken\s*=\s*(&#91;^;]*).*$)|^.*$/, '$1');
    // console.log(token);
    // token 夾帶到 headers 裡面
    // &#91;"Authorization"] 改寫成 .Authorization
    this.$http.defaults.headers.common.Authorization = token;
    // 檢查用戶是否仍然持續登入
    const api = `${process.env.VUE_APP_API}api/user/check`;
    this.$http.post(api)
      .then((res) =&gt; {
        // console.log(res);
        // 登入判斷 - 登入失敗
        if (!res.data.success) {
          // 登入失敗後會轉址到 Login 頁面
          this.$router.push('/login');
        }
      });
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">增加 Bootstrap Modal</h3>



<ol class="wp-block-list">
<li>製作新增產品的彈跳視窗<br>使用事件的方式呼叫元件</li>



<li>查看 Bootstrap 文件<br>元件 &gt; 互動視窗 Modal &gt; 完整範例 Live Demo，是透過 HTML 方式呼叫 Modal<br>因為使用 Vue.js，所以盡可能使用 JS 方式呼叫 Modal<br>元件 &gt; 互動視窗 Modal &gt; 用法 &gt; 傳遞選項<br>使用 new bootstrap modal 方法把 modal 實體化</li>



<li>在 components 資料夾新增 ProductModal.vue 檔案</li>



<li>在 Bootstrap 複製完整範例 Live Demo Modal 部分的程式碼貼到 ProductModeal.vue 檔案</li>



<li>在 ProductModal.vue 檔案加入 JS<br>著重在 JS 的部分<br>在元件新增方法讓外部元件呼叫</li>



<li>調整 ProductModal.vue 檔案 HTML 的部分<br>加上 ref 屬性，透過 ref 方式直接存取 DOM 元素</li>



<li>撰寫 ProductModal.vue 檔案 JS 的部分</li>



<li>參考 Bootstrap 文件 元件 &gt; 互動視窗 Modal &gt; 用法 &gt; 透過 JavaScript<br>複製程式碼貼到 mounted 裡面</li>



<li>在調用之前必須把 Bootstrap 的 Modal 方法載出來<br>在 node_modules/bootstrap/js/dist/modal.js 檔案，把 modal.js 檔案載進來<br>會使用 import 方法載入 Modal<br>調整 mounted 裡面程式碼，改成 new Modal</li>



<li>透過 refs方式把 DOM 元素指向外層的 ref modal<br>繼續調整 mounted 裡面程式碼<br>this.$refs.modal</li>



<li>前面的變數再指回 data 裡面定義的變數<br>改成 this.modal</li>



<li>在 mounted 加上 this.modal.show();</li>



<li>在 Products.vue 檔案<br>直接到 JS 部分直接使用 import 方式把 ProductModal 方法載進來<br>定義 components 進行 ProductModal 區域註冊<br>把 Productmodal 加到畫面上面</li>



<li>把剩餘的一些方法補上<br>showModal 方法、hideModal 方法</li>



<li>在 Products.vue 檔案<br>找到 &lt;ProductModal&gt; 標籤定義名稱 ref=”productModal”<br>使用 ref 方式直接呼叫 Modal 方法</li>



<li>在 Products.vue 檔案<br>在上方加入一些程式碼<br>使用 @click=”$refs.productModal 方式指向所定義的元件，直接呼叫裡面的 showModal 方法</li>
</ol>



<pre class="wp-block-code"><code>// components/ProductModal.vue
&lt;template&gt;
  &lt;!-- Modal --&gt;
  &lt;div class="modal fade" id="exampleModal" tabindex="-1"
  aria-labelledby="exampleModalLabel" aria-hidden="true"
  ref="modal"&gt;
    &lt;div class="modal-dialog"&gt;
      &lt;div class="modal-content"&gt;
        &lt;div class="modal-header"&gt;
          &lt;h1 class="modal-title fs-5" id="exampleModalLabel"&gt;Modal title&lt;/h1&gt;
          &lt;button type="button" class="btn-close" data-bs-dismiss="modal"
          aria-label="Close"&gt;&lt;/button&gt;
        &lt;/div&gt;
        &lt;div class="modal-body"&gt;
          ...
        &lt;/div&gt;
        &lt;div class="modal-footer"&gt;
          &lt;button type="button" class="btn btn-secondary" data-bs-dismiss="modal"&gt;Close&lt;/button&gt;
          &lt;button type="button" class="btn btn-primary"&gt;Save changes&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
// 載入 Modal
import Modal from 'bootstrap/js/dist/modal';

export default {
  data() {
    return {
      // 實體內容回傳出來
      // modal 變數定義為物件
      modal: {},
    };
  },
  // 方法
  methods: {
    showModal() {
      this.modal.show();
    },
    hideModal() {
      this.modal.hide();
    },
  },
  // 實體必須在元件載入之後才能正確運作
  // mounted 生命週期
  mounted() {
    this.modal = new Modal(this.$refs.modal);
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// views/Products.vue
&lt;template&gt;
  &lt;div class="text-end"&gt;
    &lt;button class="btn btn-primary" type="button"
    @click="$refs.productModal.showModal()"&gt;
      增加一個產品
    &lt;/button&gt;
  &lt;/div&gt;
  &lt;table class="table mt-4"&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th width="120"&gt;分類&lt;/th&gt;
        &lt;th&gt;產品名稱&lt;/th&gt;
        &lt;th width="120"&gt;原價&lt;/th&gt;
        &lt;th width="120"&gt;售價&lt;/th&gt;
        &lt;th width="100"&gt;是否啟用&lt;/th&gt;
        &lt;th width="200"&gt;編輯&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr v-for="item in products" :key="item.id"&gt;
        &lt;td&gt;{{ item.category }}&lt;/td&gt;
        &lt;td&gt;{{ item.title }}&lt;/td&gt;
        &lt;td class="text-right"&gt;
          {{ item.origin_price }}
        &lt;/td&gt;
        &lt;td class="text-right"&gt;
          {{ item.price }}
        &lt;/td&gt;
        &lt;td&gt;
          &lt;span class="text-success" v-if="item.is_enabled"&gt;啟用&lt;/span&gt;
          &lt;span class="text-muted" v-else&gt;未啟用&lt;/span&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;div class="btn-group"&gt;
            &lt;button class="btn btn-outline-primary btn-sm"&gt;編輯&lt;/button&gt;
            &lt;button class="btn btn-outline-danger btn-sm"&gt;刪除&lt;/button&gt;
          &lt;/div&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;ProductModal ref="productModal"&gt;&lt;/ProductModal&gt;
&lt;/template&gt;

&lt;script&gt;
// 載入 ProductModal
import ProductModal from '../components/ProductModal.vue';

export default {
  data() {
    return {
      // 產品資訊
      products: &#91;],
      // 分頁資訊
      pagination: {},
    };
  },
  // 定義 components 進行區域註冊
  components: {
    ProductModal,
  },
  methods: {
    // 取得產品列表是多數的產品資訊
    getProducts() {
      const api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/products`;
      this.$http.get(api)
        .then((res) =&gt; {
          if (res.data.success) {
            console.log(res.data);
            this.products = res.data.products;
            this.pagination = res.data.pagination;
          }
        });
    },
  },
  created() {
    this.getProducts();
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">透過彈出視窗新增品項</h3>



<ol class="wp-block-list">
<li>新增產品在互動邏輯是相對複雜很多</li>



<li>查看管理控制台 [需驗證] API 文件 – 商品建立</li>



<li>新增產品是如何運作<br>預期點擊右上方新增產品會跳出一個彈出視窗，輸入內容後按下確認把資料新增</li>



<li>這個章節最複雜的是有一個列表的元件，彈出視窗會有另外一個元件，這兩個元件之間的溝通是這個章節最複雜的地方</li>



<li>透過繪圖的形式了解元件之間是怎麼溝通</li>



<li>在 ProductModal.vue、Products.vue 檔案<br>在 ProductModal.vue 檔案建構表單所需要的 HTML 以及 v-model 雙向綁定</li>



<li>提醒: 上傳圖片的行為</li>



<li>在 ProductModal.vue 檔案 data() 裡面新增 tempProduct 物件進行外層的資料傳送的接收</li>



<li>在外層 Products.vue 檔案調整程式碼<br>新增 updateProduct() 方法的事件<br>調整獨立 openModal() 事件</li>



<li>openModal() 方法加到增加一個產品按鈕</li>



<li>tempProduct 資料傳送到內層<br>在內層 ProductModal.vue 建立 props 使用物件格式建立，名稱為 product<br>在外層 Products.vue data() 新增 tempProduct</li>



<li>內層所接收的 props<br>傳進來時使用 product 進行接收，product 是一個物件，預期傳進來的型別是物件 object，預設的情況下如果外層沒有正確的傳遞給予一個預設值 default() 直接回傳一個空的物件</li>



<li>props 原則是前內後外<br>在 Products.vue 檔案 &lt;ProductModal&gt; 標籤撰寫程式碼<br>:product=”tempProduct”</li>



<li>運作流程，外層的 tempProduct 透過 props 傳送進來，內層會使用 product 進行接收</li>



<li>監聽 product 內容有沒有更動，使用 watch 監聽，watch 是一個物件，監聽外層傳進來的 props<br>watch 的目的把傳進來的資料寫到 tempProduct 裡面<br>因為單向數據流不可以直接修改外層的資料<br>this.tempProduct = this.product;</li>



<li>打開開發人員工具 &gt; Vue 開發者工具<br>可以找到 &lt;ProductModal&gt;<br>按下增加一個產品，輸入表單內容就可以看到 tempProduct 有增加一些內容</li>



<li>使用 emit 事件把 tempProduct 資料傳送到遠端<br>在內層 ProductModal.vue 檔案 &lt;button&gt; 藉由按鈕觸發 emit 事件 @click=”$emit(‘update-product’)”<br>觸發 emit 事件的同時把 tempProduct 向外傳遞</li>



<li>在外層 Products.vue 預期會使用 updateProduct 進行接收，&lt;ProductModal&gt; 標籤使用 @update-product=”updateProduct”<br>使用前內後外的概念，前面是內層的元件、後面是外層所接收的函式</li>



<li>運作流程會使用 $emit 觸發外層的事件，觸發事件的名稱 updateProduct，接下來觸發 updateProduct() 函式，觸發的同時會把 tempProduct 資料內容透過參數的方式傳到 item 裡面</li>



<li>測試 tempProduct 參數有沒有正確傳過來<br>測試新增品項功能是否能正常運作</li>



<li>必須把開啟的 modal 關閉、再重新取得列表的資料</li>
</ol>



<pre class="wp-block-code"><code>// components/ProductModal.vue
&lt;template&gt;
  &lt;!-- Modal --&gt;
  &lt;div class="modal fade" id="exampleModal" tabindex="-1"
  aria-labelledby="exampleModalLabel" aria-hidden="true"
  ref="modal"&gt;
    &lt;!-- 請同學自行新增 v-model --&gt;
    &lt;div class="modal-dialog modal-xl" role="document"&gt;
      &lt;div class="modal-content border-0"&gt;
        &lt;div class="modal-header bg-dark text-white"&gt;
          &lt;h5 class="modal-title" id="exampleModalLabel"&gt;
            &lt;span&gt;新增產品&lt;/span&gt;
          &lt;/h5&gt;
          &lt;button type="button" class="btn-close"
                  data-bs-dismiss="modal" aria-label="Close"&gt;&lt;/button&gt;
        &lt;/div&gt;
        &lt;div class="modal-body"&gt;
          &lt;div class="row"&gt;
            &lt;div class="col-sm-4"&gt;
              &lt;div class="mb-3"&gt;
                &lt;label for="image" class="form-label"&gt;輸入圖片網址&lt;/label&gt;
                &lt;input type="text" class="form-control" id="image"
                        placeholder="請輸入圖片連結"
                        v-model="tempProduct.imageUrl"&gt;
              &lt;/div&gt;
              &lt;div class="mb-3"&gt;
                &lt;label for="customFile" class="form-label"&gt;或 上傳圖片
                  &lt;i class="fas fa-spinner fa-spin"&gt;&lt;/i&gt;
                &lt;/label&gt;
                &lt;input type="file" id="customFile" class="form-control"
                ref="fileInput"
                @change="uploadFile"&gt;
              &lt;/div&gt;
              &lt;img class="img-fluid" :src="tempProduct.imageUrl" alt=""&gt;
              &lt;!-- 延伸技巧，多圖 --&gt;
              &lt;div class="mt-5" v-if="tempProduct.images"&gt;
                &lt;div class="mb-3 input-group" v-for="(image, key) in tempProduct.images" :key="key"&gt;
                  &lt;input type="url" class="form-control form-control"
                          placeholder="請輸入連結"
                          v-model="tempProduct.images&#91;key]"&gt;
                  &lt;button type="button" class="btn btn-outline-danger"
                  @click="tempProduct.images.splice(key, 1)"&gt;
                    移除
                  &lt;/button&gt;
                &lt;/div&gt;
                &lt;div v-if="tempProduct.images&#91;tempProduct.images.length - 1]
                || !tempProduct.images.length"&gt;
                  &lt;button class="btn btn-outline-primary btn-sm d-block w-100"
                  @click="tempProduct.images.push('')"&gt;
                    新增圖片
                  &lt;/button&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;div class="col-sm-8"&gt;
              &lt;div class="mb-3"&gt;
                &lt;label for="title" class="form-label"&gt;標題&lt;/label&gt;
                &lt;input type="text" class="form-control" id="title"
                        placeholder="請輸入標題"
                        v-model="tempProduct.title"&gt;
              &lt;/div&gt;

              &lt;div class="row gx-2"&gt;
                &lt;div class="mb-3 col-md-6"&gt;
                  &lt;label for="category" class="form-label"&gt;分類&lt;/label&gt;
                  &lt;input type="text" class="form-control" id="category"
                          placeholder="請輸入分類"
                          v-model="tempProduct.category"&gt;
                &lt;/div&gt;
                &lt;div class="mb-3 col-md-6"&gt;
                  &lt;label for="price" class="form-label"&gt;單位&lt;/label&gt;
                  &lt;input type="text" class="form-control" id="unit"
                          placeholder="請輸入單位"
                          v-model="tempProduct.unit"&gt;
                &lt;/div&gt;
              &lt;/div&gt;

              &lt;div class="row gx-2"&gt;
                &lt;div class="mb-3 col-md-6"&gt;
                  &lt;label for="origin_price" class="form-label"&gt;原價&lt;/label&gt;
                  &lt;input type="number" class="form-control" id="origin_price"
                          placeholder="請輸入原價"
                          v-model.number="tempProduct.origin_price"&gt;
                &lt;/div&gt;
                &lt;div class="mb-3 col-md-6"&gt;
                  &lt;label for="price" class="form-label"&gt;售價&lt;/label&gt;
                  &lt;input type="number" class="form-control" id="price"
                          placeholder="請輸入售價"
                          v-model.number="tempProduct.price"&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;hr&gt;

              &lt;div class="mb-3"&gt;
                &lt;label for="description" class="form-label"&gt;產品描述&lt;/label&gt;
                &lt;textarea type="text" class="form-control" id="description"
                          placeholder="請輸入產品描述"
                          v-model="tempProduct.description"&gt;&lt;/textarea&gt;
              &lt;/div&gt;
              &lt;div class="mb-3"&gt;
                &lt;label for="content" class="form-label"&gt;說明內容&lt;/label&gt;
                &lt;textarea type="text" class="form-control" id="content"
                          placeholder="請輸入產品說明內容"
                          v-model="tempProduct.content"&gt;&lt;/textarea&gt;
              &lt;/div&gt;
              &lt;div class="mb-3"&gt;
                &lt;div class="form-check"&gt;
                  &lt;input class="form-check-input" type="checkbox"
                          :true-value="1"
                          :false-value="0"
                          id="is_enabled"
                          v-model="tempProduct.is_enabled"&gt;
                  &lt;label class="form-check-label" for="is_enabled"&gt;
                    是否啟用
                  &lt;/label&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="modal-footer"&gt;
          &lt;button type="button" class="btn btn-outline-secondary"
                  data-bs-dismiss="modal"&gt;取消
          &lt;/button&gt;
          &lt;button type="button" class="btn btn-primary"
          @click="$emit('update-product', tempProduct)"&gt;確認&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
// 載入 Modal
import Modal from 'bootstrap/js/dist/modal';

export default {
  // Props 向內層元件傳遞資料狀態
  props: {
    product: {
      type: Object,
      default() { return {}; },
    },
  },
  // 監聽
  watch: {
    product() {
      this.tempProduct = this.product;
    },
  },
  data() {
    return {
      // 實體內容回傳出來
      // modal 變數定義為物件
      modal: {},
      tempProduct: {},
    };
  },
  // 方法
  methods: {
    showModal() {
      this.modal.show();
    },
    hideModal() {
      this.modal.hide();
    },
  },
  // 實體必須在元件載入之後才能正確運作
  // mounted 生命週期
  mounted() {
    this.modal = new Modal(this.$refs.modal);
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// views/Products.vue
&lt;template&gt;
  &lt;div class="text-end"&gt;
    &lt;button class="btn btn-primary" type="button"
    @click="openModal"&gt;
      增加一個產品
    &lt;/button&gt;
  &lt;/div&gt;
  &lt;table class="table mt-4"&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th width="120"&gt;分類&lt;/th&gt;
        &lt;th&gt;產品名稱&lt;/th&gt;
        &lt;th width="120"&gt;原價&lt;/th&gt;
        &lt;th width="120"&gt;售價&lt;/th&gt;
        &lt;th width="100"&gt;是否啟用&lt;/th&gt;
        &lt;th width="200"&gt;編輯&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr v-for="item in products" :key="item.id"&gt;
        &lt;td&gt;{{ item.category }}&lt;/td&gt;
        &lt;td&gt;{{ item.title }}&lt;/td&gt;
        &lt;td class="text-right"&gt;
          {{ item.origin_price }}
        &lt;/td&gt;
        &lt;td class="text-right"&gt;
          {{ item.price }}
        &lt;/td&gt;
        &lt;td&gt;
          &lt;span class="text-success" v-if="item.is_enabled"&gt;啟用&lt;/span&gt;
          &lt;span class="text-muted" v-else&gt;未啟用&lt;/span&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;div class="btn-group"&gt;
            &lt;button class="btn btn-outline-primary btn-sm"&gt;編輯&lt;/button&gt;
            &lt;button class="btn btn-outline-danger btn-sm"&gt;刪除&lt;/button&gt;
          &lt;/div&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;ProductModal ref="productModal"
  :product="tempProduct"
  @update-product="updateProduct"&gt;&lt;/ProductModal&gt;
&lt;/template&gt;

&lt;script&gt;
// 載入 ProductModal
import ProductModal from '../components/ProductModal.vue';

export default {
  data() {
    return {
      // 產品資訊
      products: &#91;],
      // 分頁資訊
      pagination: {},
      tempProduct: {},
    };
  },
  // 定義 components 進行區域註冊
  components: {
    ProductModal,
  },
  methods: {
    // 取得產品列表是多數的產品資訊
    getProducts() {
      const api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/products`;
      this.$http.get(api)
        .then((res) =&gt; {
          if (res.data.success) {
            console.log(res.data);
            this.products = res.data.products;
            this.pagination = res.data.pagination;
          }
        });
    },
    openModal() {
      this.tempProduct = {};
      const productComponent = this.$refs.productModal;
      productComponent.showModal();
    },
    updateProduct(item) {
      // 測試 tempProduct 參數有沒有正確傳過來
      // console.log(item);
      this.tempProduct = item;
      const api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/product`;
      const productComponent = this.$refs.productModal;
      this.$http.post(api, { data: this.tempProduct })
        .then((response) =&gt; {
          console.log(response);
          // modal 關閉
          productComponent.hideModal();
          // 重新取得列表資料
          this.getProducts();
        });
    },
  },
  created() {
    this.getProducts();
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">產品資料更新</h3>



<ol class="wp-block-list">
<li>在 Products.vue 檔案的 tempProduct不是只為了新增，還為了編輯做使用</li>



<li>在範例點擊其中一個品項的編輯，會把這一個產品的品項新增到 tempProduct，送出的不是空物件，而是所點擊的這個產品的品項，因此這個 tempProduct 主要的用途是為了編輯做使用</li>



<li>查看管理控制台 [需驗證] API 文件 &gt; 修改產品<br>查看 [API]、[方法] 與商品建立的差異<br>調整程式碼撰寫修改產品的功能</li>



<li>主要調整產品列表的部分<br>在 Products.vue 檔案<br>會透過屬性來判斷目前是否是新增的狀態，在 data() 新增一個 isNew 的狀態，目前是 false</li>



<li>在 openModal 新增兩個參數，一個是不是新的、另一個是編輯的話，把編輯的品項加進來<br>使用 console 查看<br>在 HTML 的 openModal 加入參數 true<br>@click=”openModal(true)<br>在品項的編輯按鈕加上 @click=”openModal(false, item)，false、以及當前的品項<br>當前的品項是把 v-for 裡面的 item 帶到編輯裡面</li>



<li>在 openModal(isNew, item) 加上流程判斷、調整程式碼<br>測試編輯功能是否能正確運作</li>



<li>調整更新的部分<br>查看管理控制台 [需驗證] API 文件 – 修改產品<br>[API]、[方法]<br>使用 this.isNew 流程判斷目前是新增還是修改<br>調整 updateProduct(item) 方法程式碼<br>測試編輯更新功能是否能正確運作</li>
</ol>



<pre class="wp-block-code"><code>// views/Products.vue
&lt;template&gt;
  &lt;div class="text-end"&gt;
    &lt;button class="btn btn-primary" type="button"
    @click="openModal(true)"&gt;
      增加一個產品
    &lt;/button&gt;
  &lt;/div&gt;
  &lt;table class="table mt-4"&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th width="120"&gt;分類&lt;/th&gt;
        &lt;th&gt;產品名稱&lt;/th&gt;
        &lt;th width="120"&gt;原價&lt;/th&gt;
        &lt;th width="120"&gt;售價&lt;/th&gt;
        &lt;th width="100"&gt;是否啟用&lt;/th&gt;
        &lt;th width="200"&gt;編輯&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr v-for="item in products" :key="item.id"&gt;
        &lt;td&gt;{{ item.category }}&lt;/td&gt;
        &lt;td&gt;{{ item.title }}&lt;/td&gt;
        &lt;td class="text-right"&gt;
          {{ item.origin_price }}
        &lt;/td&gt;
        &lt;td class="text-right"&gt;
          {{ item.price }}
        &lt;/td&gt;
        &lt;td&gt;
          &lt;span class="text-success" v-if="item.is_enabled"&gt;啟用&lt;/span&gt;
          &lt;span class="text-muted" v-else&gt;未啟用&lt;/span&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;div class="btn-group"&gt;
            &lt;button class="btn btn-outline-primary btn-sm"
            @click="openModal(false, item)"&gt;編輯&lt;/button&gt;
            &lt;button class="btn btn-outline-danger btn-sm"&gt;刪除&lt;/button&gt;
          &lt;/div&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;ProductModal ref="productModal"
  :product="tempProduct"
  @update-product="updateProduct"&gt;&lt;/ProductModal&gt;
&lt;/template&gt;

&lt;script&gt;
// 載入 ProductModal
import ProductModal from '../components/ProductModal.vue';

export default {
  data() {
    return {
      // 產品資訊
      products: &#91;],
      // 分頁資訊
      pagination: {},
      tempProduct: {},
      // 目前狀態
      isNew: false,
    };
  },
  // 定義 components 進行區域註冊
  components: {
    ProductModal,
  },
  methods: {
    // 取得產品列表是多數的產品資訊
    getProducts() {
      const api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/products`;
      this.$http.get(api)
        .then((res) =&gt; {
          if (res.data.success) {
            console.log(res.data);
            this.products = res.data.products;
            this.pagination = res.data.pagination;
          }
        });
    },
    openModal(isNew, item) {
      // console.log(isNew, item);
      // 流程判斷
      if (isNew) {
        this.tempProduct = {};
      } else {
        // 使用展開的形式取出資料
        this.tempProduct = { ...item };
      }
      this.isNew = isNew;
      const productComponent = this.$refs.productModal;
      productComponent.showModal();
    },
    updateProduct(item) {
      // 測試 tempProduct 參數有沒有正確傳過來
      // console.log(item);
      this.tempProduct = item;
      // 新增
      let api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/product`;
      let httpMethod = 'post';
      // 編輯
      if (!this.isNes) {
        api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/product/${item.id}`;
        httpMethod = 'put';
      }
      const productComponent = this.$refs.productModal;
      this.$http&#91;httpMethod](api, { data: this.tempProduct })
        .then((response) =&gt; {
          console.log(response);
          // modal 關閉
          productComponent.hideModal();
          // 重新取得列表資料
          this.getProducts();
        });
    },
  },
  created() {
    this.getProducts();
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">透過 API 上傳圖片</h3>



<ol class="wp-block-list">
<li>介紹上傳圖片的功能</li>



<li>查看管理控制台 [需驗證] API 文件 &gt; 上傳圖片<br>上傳表單是使用傳統的 &lt;form&gt; 標籤上傳的時候會用到<br>action 是 API 路徑、enctype 是使用 form-data 格式、method 使用的方法是 post<br>input 欄位有 name 屬性是 file-to-upload，是 API 上傳檔案需要對應的欄位<br>表單傳送 action</li>



<li>在 ProductModal.vue 檔案<br>觀看上傳圖片部分的程式碼，提及&lt;input&gt; @chagne=”uploadFile”、uploadFile() 方法</li>



<li>著重在如何把檔案取出，並轉成 form-data 格式<br>首要條件是取得 input 的檔案內容，在這 &lt;input&gt; 先定義 ref=”fileInput”，便於取得這個 DOM 元素，並且把檔案取出來使用</li>



<li>在 ProductModal.vue 檔案 JS 部分著重在 uploadFile 函式<br>把 input 裡面的內容取出<br>使用 console.dir(uploadFile) 查看，上傳任意圖片測試，查找 files 屬性，是一個陣列，要取得的是第0個檔案，再次上傳任意圖片測試</li>



<li>取出的檔案轉成 form-data 格式<br>宣告 formData 變數使用 new FormData() JS 方法<br>formData.append，append 是要增加一個欄位到表單裡面，欄位的名稱是 API 文件 name=”file-to-upload”，欄位內容是取出來的檔案</li>



<li>透過 API 形式發送到遠端<br>測試 console.log(response.data);</li>
</ol>



<pre class="wp-block-code"><code>// components/ProductModal.vue
&lt;template&gt;
  &lt;!-- Modal --&gt;
  &lt;div class="modal fade" id="exampleModal" tabindex="-1"
  aria-labelledby="exampleModalLabel" aria-hidden="true"
  ref="modal"&gt;
    &lt;!-- 請同學自行新增 v-model --&gt;
    &lt;div class="modal-dialog modal-xl" role="document"&gt;
      &lt;div class="modal-content border-0"&gt;
        &lt;div class="modal-header bg-dark text-white"&gt;
          &lt;h5 class="modal-title" id="exampleModalLabel"&gt;
            &lt;span&gt;新增產品&lt;/span&gt;
          &lt;/h5&gt;
          &lt;button type="button" class="btn-close"
                  data-bs-dismiss="modal" aria-label="Close"&gt;&lt;/button&gt;
        &lt;/div&gt;
        &lt;div class="modal-body"&gt;
          &lt;div class="row"&gt;
            &lt;div class="col-sm-4"&gt;
              &lt;div class="mb-3"&gt;
                &lt;label for="image" class="form-label"&gt;輸入圖片網址&lt;/label&gt;
                &lt;input type="text" class="form-control" id="image"
                        placeholder="請輸入圖片連結"
                        v-model="tempProduct.imageUrl"&gt;
              &lt;/div&gt;
              &lt;div class="mb-3"&gt;
                &lt;label for="customFile" class="form-label"&gt;或 上傳圖片
                  &lt;i class="fas fa-spinner fa-spin"&gt;&lt;/i&gt;
                &lt;/label&gt;
                &lt;input type="file" id="customFile" class="form-control"
                ref="fileInput"
                @change="uploadFile"&gt;
              &lt;/div&gt;
              &lt;img class="img-fluid" :src="tempProduct.imageUrl" alt=""&gt;
              &lt;!-- 延伸技巧，多圖 --&gt;
              &lt;div class="mt-5" v-if="tempProduct.images"&gt;
                &lt;div class="mb-3 input-group" v-for="(image, key) in tempProduct.images" :key="key"&gt;
                  &lt;input type="url" class="form-control form-control"
                          placeholder="請輸入連結"
                          v-model="tempProduct.images&#91;key]"&gt;
                  &lt;button type="button" class="btn btn-outline-danger"
                  @click="tempProduct.images.splice(key, 1)"&gt;
                    移除
                  &lt;/button&gt;
                &lt;/div&gt;
                &lt;div v-if="tempProduct.images&#91;tempProduct.images.length - 1]
                || !tempProduct.images.length"&gt;
                  &lt;button class="btn btn-outline-primary btn-sm d-block w-100"
                  @click="tempProduct.images.push('')"&gt;
                    新增圖片
                  &lt;/button&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;div class="col-sm-8"&gt;
              &lt;div class="mb-3"&gt;
                &lt;label for="title" class="form-label"&gt;標題&lt;/label&gt;
                &lt;input type="text" class="form-control" id="title"
                        placeholder="請輸入標題"
                        v-model="tempProduct.title"&gt;
              &lt;/div&gt;

              &lt;div class="row gx-2"&gt;
                &lt;div class="mb-3 col-md-6"&gt;
                  &lt;label for="category" class="form-label"&gt;分類&lt;/label&gt;
                  &lt;input type="text" class="form-control" id="category"
                          placeholder="請輸入分類"
                          v-model="tempProduct.category"&gt;
                &lt;/div&gt;
                &lt;div class="mb-3 col-md-6"&gt;
                  &lt;label for="price" class="form-label"&gt;單位&lt;/label&gt;
                  &lt;input type="text" class="form-control" id="unit"
                          placeholder="請輸入單位"
                          v-model="tempProduct.unit"&gt;
                &lt;/div&gt;
              &lt;/div&gt;

              &lt;div class="row gx-2"&gt;
                &lt;div class="mb-3 col-md-6"&gt;
                  &lt;label for="origin_price" class="form-label"&gt;原價&lt;/label&gt;
                  &lt;input type="number" class="form-control" id="origin_price"
                          placeholder="請輸入原價"
                          v-model.number="tempProduct.origin_price"&gt;
                &lt;/div&gt;
                &lt;div class="mb-3 col-md-6"&gt;
                  &lt;label for="price" class="form-label"&gt;售價&lt;/label&gt;
                  &lt;input type="number" class="form-control" id="price"
                          placeholder="請輸入售價"
                          v-model.number="tempProduct.price"&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;hr&gt;

              &lt;div class="mb-3"&gt;
                &lt;label for="description" class="form-label"&gt;產品描述&lt;/label&gt;
                &lt;textarea type="text" class="form-control" id="description"
                          placeholder="請輸入產品描述"
                          v-model="tempProduct.description"&gt;&lt;/textarea&gt;
              &lt;/div&gt;
              &lt;div class="mb-3"&gt;
                &lt;label for="content" class="form-label"&gt;說明內容&lt;/label&gt;
                &lt;textarea type="text" class="form-control" id="content"
                          placeholder="請輸入產品說明內容"
                          v-model="tempProduct.content"&gt;&lt;/textarea&gt;
              &lt;/div&gt;
              &lt;div class="mb-3"&gt;
                &lt;div class="form-check"&gt;
                  &lt;input class="form-check-input" type="checkbox"
                          :true-value="1"
                          :false-value="0"
                          id="is_enabled"
                          v-model="tempProduct.is_enabled"&gt;
                  &lt;label class="form-check-label" for="is_enabled"&gt;
                    是否啟用
                  &lt;/label&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="modal-footer"&gt;
          &lt;button type="button" class="btn btn-outline-secondary"
                  data-bs-dismiss="modal"&gt;取消
          &lt;/button&gt;
          &lt;button type="button" class="btn btn-primary"
          @click="$emit('update-product', tempProduct)"&gt;確認&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
// 載入 Modal
import Modal from 'bootstrap/js/dist/modal';

export default {
  // Props 向內層元件傳遞資料狀態
  props: {
    product: {
      type: Object,
      default() { return {}; },
    },
  },
  // 監聽
  watch: {
    product() {
      this.tempProduct = this.product;
      // 多圖範例
      if (!this.tempProduct.images) {
        this.tempProduct.images = &#91;];
      }
    },
  },
  data() {
    return {
      // 實體內容回傳出來
      // modal 變數定義為物件
      modal: {},
      tempProduct: {},
    };
  },
  // 方法
  methods: {
    showModal() {
      this.modal.show();
    },
    hideModal() {
      this.modal.hide();
    },
    uploadFile() {
      // 取出上傳的檔案
      const uploadFile = this.$refs.fileInput.files&#91;0];
      // console.dir(uploadFile);
      // 轉成 form-data 格式
      const formData = new FormData();
      formData.append('file-to-upload', uploadFile);
      // 透過 API 形式發送到遠端
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/upload`;
      this.$http.post(url, formData).then((response) =&gt; {
        // console.log(response.data);
        if (response.data.success) {
          this.tempProduct.imageUrl = response.data.imageUrl;
        }
      });
    },
  },
  // 實體必須在元件載入之後才能正確運作
  // mounted 生命週期
  mounted() {
    this.modal = new Modal(this.$refs.modal);
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">使用 mixin 整合相同程式碼</h3>



<ol class="wp-block-list">
<li>元件裡面有相同的程式碼要怎麼樣進行合併</li>



<li>試著完成 delModal 功能</li>



<li>DelModal.vue 和 ProductModal.vue 相同片段的程式碼<br>使用 vue 的特性 mixin 把相同的程式碼抽離出來</li>



<li>在 src 新增資料夾 mixins<br>在 mixins 新增檔案 modalMixin.js</li>



<li>抽離 DelModal.vue 檔案相同程式碼的部分到 modalMixin.js 檔案</li>



<li>在 DelModal.vue 檔案匯入 modalMixin.js 檔案<br>在元件加入 mixins 屬性，是一個陣列<br>mixins: [modalMixin]</li>



<li>使用相同的方式把 ProductModal.vue 檔案相同的程式碼也抽離出來</li>
</ol>



<pre class="wp-block-code"><code>// components/DelModal.vue - 2. 試著完成 delModal 功能
&lt;template&gt;
  &lt;div class="modal fade" id="delModal" tabindex="-1" role="dialog"
  aria-labelledby="exampleModalLabel" aria-hidden="true" ref="modal"&gt;
    &lt;div class="modal-dialog" role="document"&gt;
      &lt;div class="modal-content border-0"&gt;
        &lt;div class="modal-header bg-danger text-white"&gt;
          &lt;h5 class="modal-title"&gt;
            &lt;span&gt;刪除 {{ item.title }}&lt;/span&gt;
          &lt;/h5&gt;
          &lt;button type="button" class="btn-close"
          data-bs-dismiss="modal" aria-label="Close"&gt;&lt;/button&gt;
        &lt;/div&gt;
        &lt;div class="modal-body"&gt;
          是否刪除 &lt;strong class="text-danger"&gt;{{ item.title }}&lt;/strong&gt; (刪除後將無法恢復)。
        &lt;/div&gt;
        &lt;div class="modal-footer"&gt;
          &lt;button type="button" class="btn btn-outline-secondary"
          data-bs-dismiss="modal"&gt;取消
          &lt;/button&gt;
          &lt;button type="button" class="btn btn-danger"
          @click="$emit('del-item')"&gt;確認刪除
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import Modal from 'bootstrap/js/dist/modal';

export default {
  props: {
    item: {},
  },
  data() {
    return {
      modal: '',
    };
  },
  methods: {
    showModal() {
      this.modal.show();
    },
    hideModal() {
      this.modal.hide();
    },
  },
  mounted() {
    this.modal = new Modal(this.$refs.modal);
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// mixins/modalMixin.js
import Modal from 'bootstrap/js/dist/modal';

export default {
  methods: {
    showModal() {
      this.modal.show();
    },
    hideModal() {
      this.modal.hide();
    },
  },
  mounted() {
    this.modal = new Modal(this.$refs.modal);
  },
};
</code></pre>



<pre class="wp-block-code"><code>// components/DelModal.vue
&lt;template&gt;
  &lt;div class="modal fade" id="delModal" tabindex="-1" role="dialog"
  aria-labelledby="exampleModalLabel" aria-hidden="true" ref="modal"&gt;
    &lt;div class="modal-dialog" role="document"&gt;
      &lt;div class="modal-content border-0"&gt;
        &lt;div class="modal-header bg-danger text-white"&gt;
          &lt;h5 class="modal-title"&gt;
            &lt;span&gt;刪除 {{ item.title }}&lt;/span&gt;
          &lt;/h5&gt;
          &lt;button type="button" class="btn-close"
          data-bs-dismiss="modal" aria-label="Close"&gt;&lt;/button&gt;
        &lt;/div&gt;
        &lt;div class="modal-body"&gt;
          是否刪除 &lt;strong class="text-danger"&gt;{{ item.title }}&lt;/strong&gt; (刪除後將無法恢復)。
        &lt;/div&gt;
        &lt;div class="modal-footer"&gt;
          &lt;button type="button" class="btn btn-outline-secondary"
          data-bs-dismiss="modal"&gt;取消
          &lt;/button&gt;
          &lt;button type="button" class="btn btn-danger"
          @click="$emit('del-item')"&gt;確認刪除
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import modalMixin from '@/mixins/modalMixin';

export default {
  props: {
    item: {},
  },
  data() {
    return {
      modal: '',
    };
  },
  // 使用 mixins 整合相同程式碼
  mixins: &#91;modalMixin],
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// components/ProductModal.vue
&lt;template&gt;
  &lt;!-- Modal --&gt;
  &lt;div class="modal fade" id="exampleModal" tabindex="-1"
  aria-labelledby="exampleModalLabel" aria-hidden="true"
  ref="modal"&gt;
    &lt;!-- 請同學自行新增 v-model --&gt;
    &lt;div class="modal-dialog modal-xl" role="document"&gt;
      &lt;div class="modal-content border-0"&gt;
        &lt;div class="modal-header bg-dark text-white"&gt;
          &lt;h5 class="modal-title" id="exampleModalLabel"&gt;
            &lt;span&gt;新增產品&lt;/span&gt;
          &lt;/h5&gt;
          &lt;button type="button" class="btn-close"
                  data-bs-dismiss="modal" aria-label="Close"&gt;&lt;/button&gt;
        &lt;/div&gt;
        &lt;div class="modal-body"&gt;
          &lt;div class="row"&gt;
            &lt;div class="col-sm-4"&gt;
              &lt;div class="mb-3"&gt;
                &lt;label for="image" class="form-label"&gt;輸入圖片網址&lt;/label&gt;
                &lt;input type="text" class="form-control" id="image"
                        placeholder="請輸入圖片連結"
                        v-model="tempProduct.imageUrl"&gt;
              &lt;/div&gt;
              &lt;div class="mb-3"&gt;
                &lt;label for="customFile" class="form-label"&gt;或 上傳圖片
                  &lt;i class="fas fa-spinner fa-spin"&gt;&lt;/i&gt;
                &lt;/label&gt;
                &lt;input type="file" id="customFile" class="form-control"
                ref="fileInput"
                @change="uploadFile"&gt;
              &lt;/div&gt;
              &lt;img class="img-fluid" :src="tempProduct.imageUrl" alt=""&gt;
              &lt;!-- 延伸技巧，多圖 --&gt;
              &lt;div class="mt-5" v-if="tempProduct.images"&gt;
                &lt;div class="mb-3 input-group" v-for="(image, key) in tempProduct.images" :key="key"&gt;
                  &lt;input type="url" class="form-control form-control"
                          placeholder="請輸入連結"
                          v-model="tempProduct.images&#91;key]"&gt;
                  &lt;button type="button" class="btn btn-outline-danger"
                  @click="tempProduct.images.splice(key, 1)"&gt;
                    移除
                  &lt;/button&gt;
                &lt;/div&gt;
                &lt;div v-if="tempProduct.images&#91;tempProduct.images.length - 1]
                || !tempProduct.images.length"&gt;
                  &lt;button class="btn btn-outline-primary btn-sm d-block w-100"
                  @click="tempProduct.images.push('')"&gt;
                    新增圖片
                  &lt;/button&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;div class="col-sm-8"&gt;
              &lt;div class="mb-3"&gt;
                &lt;label for="title" class="form-label"&gt;標題&lt;/label&gt;
                &lt;input type="text" class="form-control" id="title"
                        placeholder="請輸入標題"
                        v-model="tempProduct.title"&gt;
              &lt;/div&gt;

              &lt;div class="row gx-2"&gt;
                &lt;div class="mb-3 col-md-6"&gt;
                  &lt;label for="category" class="form-label"&gt;分類&lt;/label&gt;
                  &lt;input type="text" class="form-control" id="category"
                          placeholder="請輸入分類"
                          v-model="tempProduct.category"&gt;
                &lt;/div&gt;
                &lt;div class="mb-3 col-md-6"&gt;
                  &lt;label for="price" class="form-label"&gt;單位&lt;/label&gt;
                  &lt;input type="text" class="form-control" id="unit"
                          placeholder="請輸入單位"
                          v-model="tempProduct.unit"&gt;
                &lt;/div&gt;
              &lt;/div&gt;

              &lt;div class="row gx-2"&gt;
                &lt;div class="mb-3 col-md-6"&gt;
                  &lt;label for="origin_price" class="form-label"&gt;原價&lt;/label&gt;
                  &lt;input type="number" class="form-control" id="origin_price"
                          placeholder="請輸入原價"
                          v-model.number="tempProduct.origin_price"&gt;
                &lt;/div&gt;
                &lt;div class="mb-3 col-md-6"&gt;
                  &lt;label for="price" class="form-label"&gt;售價&lt;/label&gt;
                  &lt;input type="number" class="form-control" id="price"
                          placeholder="請輸入售價"
                          v-model.number="tempProduct.price"&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;hr&gt;

              &lt;div class="mb-3"&gt;
                &lt;label for="description" class="form-label"&gt;產品描述&lt;/label&gt;
                &lt;textarea type="text" class="form-control" id="description"
                          placeholder="請輸入產品描述"
                          v-model="tempProduct.description"&gt;&lt;/textarea&gt;
              &lt;/div&gt;
              &lt;div class="mb-3"&gt;
                &lt;label for="content" class="form-label"&gt;說明內容&lt;/label&gt;
                &lt;textarea type="text" class="form-control" id="content"
                          placeholder="請輸入產品說明內容"
                          v-model="tempProduct.content"&gt;&lt;/textarea&gt;
              &lt;/div&gt;
              &lt;div class="mb-3"&gt;
                &lt;div class="form-check"&gt;
                  &lt;input class="form-check-input" type="checkbox"
                          :true-value="1"
                          :false-value="0"
                          id="is_enabled"
                          v-model="tempProduct.is_enabled"&gt;
                  &lt;label class="form-check-label" for="is_enabled"&gt;
                    是否啟用
                  &lt;/label&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="modal-footer"&gt;
          &lt;button type="button" class="btn btn-outline-secondary"
                  data-bs-dismiss="modal"&gt;取消
          &lt;/button&gt;
          &lt;button type="button" class="btn btn-primary"
          @click="$emit('update-product', tempProduct)"&gt;確認&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import modalMixin from '@/mixins/modalMixin';

export default {
  // Props 向內層元件傳遞資料狀態
  props: {
    product: {
      type: Object,
      default() { return {}; },
    },
  },
  // 監聽
  watch: {
    product() {
      this.tempProduct = this.product;
      // 多圖範例
      if (!this.tempProduct.images) {
        this.tempProduct.images = &#91;];
      }
    },
  },
  data() {
    return {
      // 實體內容回傳出來
      // modal 變數定義為物件
      modal: {},
      tempProduct: {},
    };
  },

  // 使用 mixinx 整合相同程式碼
  mixins: &#91;modalMixin],
  // 方法
  methods: {
    uploadFile() {
      // 取出上傳的檔案
      const uploadFile = this.$refs.fileInput.files&#91;0];
      // console.dir(uploadFile);
      // 轉成 form-data 格式
      const formData = new FormData();
      formData.append('file-to-upload', uploadFile);
      // 透過 API 形式發送到遠端
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/upload`;
      this.$http.post(url, formData).then((response) =&gt; {
        // console.log(response.data);
        if (response.data.success) {
          this.tempProduct.imageUrl = response.data.imageUrl;
        }
      });
    },
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">加入讀取的視覺效果</h3>



<ol class="wp-block-list">
<li>使用 vue3-loading-overlay 套件</li>



<li>安裝 vue3-loading-overlay 套件<br>npm install vue3-loading-overlay</li>



<li>查看文件是如何使用讀取效果</li>



<li>載入元件 – Import component<br>複製 vue3-loading-over Usage 的程式碼<br>打開 main.js 檔案，然後貼上程式碼<br>修改程式碼路徑<br>註冊元件，使用全域註冊方式啟用元件</li>



<li>在 Products.vue 檔案把 Loading 元件加到最上方<br>加上 active 狀態，這是一個 props，實際上會把自定義的狀態給傳進去，自定義狀態名稱<br>把狀態加到 data() 裡面</li>



<li>預期在取得產品列表的時候將讀取的效果顯示<br>在 getProducts() 將讀取的效果加上</li>



<li>可以為所有的 AJAX 行為都加上讀取效果，包含登入、註冊、新增產品、編輯、刪除</li>
</ol>



<pre class="wp-block-code"><code>// src/main.js
import { createApp } from 'vue';
import axios from 'axios';
import VueAxios from 'vue-axios';
import Loading from 'vue3-loading-overlay';
import 'vue3-loading-overlay/dist/vue3-loading-overlay.css';
import App from './App.vue';
import router from './router';

const app = createApp(App);
app.use(VueAxios, axios);
app.use(router);
app.component('Loading', Loading);
app.mount('#app');
</code></pre>



<pre class="wp-block-code"><code>// views/Products.vue
&lt;template&gt;
  &lt;Loading :active="isLoading"&gt;&lt;/Loading&gt;
  &lt;div class="text-end mt-3"&gt;
    &lt;button class="btn btn-primary" type="button"
    @click="openModal(true)"&gt;
      增加一個產品
    &lt;/button&gt;
  &lt;/div&gt;
  &lt;table class="table mt-4"&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th width="120"&gt;分類&lt;/th&gt;
        &lt;th&gt;產品名稱&lt;/th&gt;
        &lt;th width="120"&gt;原價&lt;/th&gt;
        &lt;th width="120"&gt;售價&lt;/th&gt;
        &lt;th width="100"&gt;是否啟用&lt;/th&gt;
        &lt;th width="200"&gt;編輯&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr v-for="item in products" :key="item.id"&gt;
        &lt;td&gt;{{ item.category }}&lt;/td&gt;
        &lt;td&gt;{{ item.title }}&lt;/td&gt;
        &lt;td class="text-right"&gt;
          {{ item.origin_price }}
        &lt;/td&gt;
        &lt;td class="text-right"&gt;
          {{ item.price }}
        &lt;/td&gt;
        &lt;td&gt;
          &lt;span class="text-success" v-if="item.is_enabled"&gt;啟用&lt;/span&gt;
          &lt;span class="text-muted" v-else&gt;未啟用&lt;/span&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;div class="btn-group"&gt;
            &lt;button class="btn btn-outline-primary btn-sm"
            @click="openModal(false, item)"&gt;編輯&lt;/button&gt;
            &lt;button class="btn btn-outline-danger btn-sm"
            @click="openDelProductModal(item)"&gt;刪除&lt;/button&gt;
          &lt;/div&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;ProductModal ref="productModal"
  :product="tempProduct"
  @update-product="updateProduct"&gt;&lt;/ProductModal&gt;
  &lt;DelModal ref="delModal"
  :item="tempProduct"
  @del-item="delProduct"&gt;&lt;/DelModal&gt;
&lt;/template&gt;

&lt;script&gt;
// 載入 ProductModal
import ProductModal from '@/components/ProductModal.vue';
import DelModal from '@/components/DelModal.vue';

export default {
  data() {
    return {
      // 產品資訊
      products: &#91;],
      // 分頁資訊
      pagination: {},
      tempProduct: {},
      // 目前狀態
      isNew: false,
      // 讀取狀態
      isLoading: false,
    };
  },
  // 定義 components 進行區域註冊
  components: {
    ProductModal,
    DelModal,
  },
  methods: {
    // 取得產品列表是多數的產品資訊
    getProducts() {
      const api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/products`;
      // 讀取效果
      this.isLoading = true;
      this.$http.get(api)
        .then((res) =&gt; {
          this.isLoading = false;
          if (res.data.success) {
            console.log(res.data);
            this.products = res.data.products;
            this.pagination = res.data.pagination;
          }
        });
    },
    openModal(isNew, item) {
      // console.log(isNew, item);
      // 流程判斷
      if (isNew) {
        this.tempProduct = {};
      } else {
        // 使用展開的形式取出資料
        this.tempProduct = { ...item };
      }
      this.isNew = isNew;
      const productComponent = this.$refs.productModal;
      productComponent.showModal();
    },
    updateProduct(item) {
      // 測試 tempProduct 參數有沒有正確傳過來
      // console.log(item);
      this.tempProduct = item;
      // 新增
      let api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/product`;
      let httpMethod = 'post';
      // 編輯
      if (!this.isNew) {
        api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/product/${item.id}`;
        httpMethod = 'put';
      }
      const productComponent = this.$refs.productModal;
      this.$http&#91;httpMethod](api, { data: this.tempProduct })
        .then((response) =&gt; {
          console.log(response);
          // modal 關閉
          productComponent.hideModal();
          // 重新取得列表資料
          this.getProducts();
        });
    },
    // 開啟刪除 Modal
    openDelProductModal(item) {
      this.tempProduct = { ...item };
      const delComponent = this.$refs.delModal;
      delComponent.showModal();
    },
    delProduct() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/product/${this.tempProduct.id}`;
      this.$http.delete(url).then((response) =&gt; {
        console.log(response.data);
        const delComponent = this.$refs.delModal;
        delComponent.hideModal();
        this.getProducts();
      });
    },
  },
  created() {
    this.getProducts();
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">加入錯誤的訊息回饋</h3>



<ol class="wp-block-list">
<li>錯誤通知算是相對複雜的章節<br>主要原因會牽動到非常多的檔案的互相溝通</li>



<li>使用 Bootstrap 吐司 (Toasts) 元件<br>元件 &gt; 吐司 &gt; 方法</li>



<li>透過講解的形式了解吐司元件是怎麼運作<br>吐司元件跟 Vue 元件如何互動<br>目的是讓所有的元件都可以使用這個通知的功能<br>通知功能不會只掛載在特定的元件下，會獨立放在任何可以呼叫到的地方，是獨立元件<br>可以堆疊產生多個通知<br>分離可以產生堆疊，每個吐司有自己獨立的生命週期，啟用的時候可以使用參數<br>產品列表與吐司元件可以使用到 mitt 套件，進行跨元件的溝通</li>



<li>原始碼是如何運作<br>安裝 mitt 套件，功能為跨元件溝通使用<br>新增 emitter.js 檔案、撰寫程式碼<br>在元件利用的時候可以只加在最外層，在 Dashboard 檔案匯入 emitter，使用 provide 讓內層的元件都可以使用外層功能<br>在 Products.vue 檔案使用 inject<br>在 ToastMessages.vue 檔案使用 inject</li>



<li>新增 ToastMessages.vue 檔案以及程式碼內容<br>作為定位使用、列表呈現<br>在 mounted() 生命週期加上 emitter 事件</li>



<li>新增 Toast.vue 檔案以及程式碼內容<br>吐司元件<br>從 Bootstrap 文件複製程式碼<br>最重要的地方每次吐司元件生成的時候，觸發屬於自己生命周期的時候，吐司元件會開啟 6 秒鐘後消失</li>



<li>Products 如何把訊息傳給吐司元件<br>如何透過 mitt 送到吐司列表裡面<br>在 Products.vue 檔案撰寫程式碼，根據判斷狀態推送不同的訊息內容</li>



<li>可以參考課程範例逐步拆解流程該怎麼樣運作</li>
</ol>



<pre class="wp-block-code"><code>// src/methods/emitter.js
import mitt from 'mitt';

const emitter = mitt();

export default emitter;
</code></pre>



<pre class="wp-block-code"><code>// views/Dashboard.vue
&lt;template&gt;
  &lt;Navbar&gt;&lt;/Navbar&gt;
  &lt;div class="container-fluid mt-3 position-relative"&gt;
    &lt;ToastMessages&gt;&lt;/ToastMessages&gt;
    &lt;router-view/&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import emitter from '@/methods/emitter';
import ToastMessages from '@/components/ToastMessages.vue';
import Navbar from '../components/Navbar.vue';

export default {
  components: {
    Navbar,
    ToastMessages,
  },
  // provide
  provide() {
    return {
      emitter,
    };
  },
  created() {
    // 取出 token
    // test2 改寫成 hexToken
    // Airbnb 規範把錯誤的 \ 去除
    const token = document.cookie.replace(/(?:(?:^|.*;\s*)hexToken\s*=\s*(&#91;^;]*).*$)|^.*$/, '$1');
    // console.log(token);
    // token 夾帶到 headers 裡面
    // &#91;"Authorization"] 改寫成 .Authorization
    this.$http.defaults.headers.common.Authorization = token;
    // 檢查用戶是否仍然持續登入
    const api = `${process.env.VUE_APP_API}api/user/check`;
    this.$http.post(api)
      .then((res) =&gt; {
        // console.log(res);
        // 登入判斷 - 登入失敗
        if (!res.data.success) {
          // 登入失敗後會轉址到 Login 頁面
          this.$router.push('/login');
        }
      });
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// views/Products.vue
&lt;template&gt;
  &lt;Loading :active="isLoading"&gt;&lt;/Loading&gt;
  &lt;div class="text-end mt-3"&gt;
    &lt;button class="btn btn-primary" type="button"
    @click="openModal(true)"&gt;
      增加一個產品
    &lt;/button&gt;
  &lt;/div&gt;
  &lt;table class="table mt-4"&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th width="120"&gt;分類&lt;/th&gt;
        &lt;th&gt;產品名稱&lt;/th&gt;
        &lt;th width="120"&gt;原價&lt;/th&gt;
        &lt;th width="120"&gt;售價&lt;/th&gt;
        &lt;th width="100"&gt;是否啟用&lt;/th&gt;
        &lt;th width="200"&gt;編輯&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr v-for="item in products" :key="item.id"&gt;
        &lt;td&gt;{{ item.category }}&lt;/td&gt;
        &lt;td&gt;{{ item.title }}&lt;/td&gt;
        &lt;td class="text-right"&gt;
          {{ item.origin_price }}
        &lt;/td&gt;
        &lt;td class="text-right"&gt;
          {{ item.price }}
        &lt;/td&gt;
        &lt;td&gt;
          &lt;span class="text-success" v-if="item.is_enabled"&gt;啟用&lt;/span&gt;
          &lt;span class="text-muted" v-else&gt;未啟用&lt;/span&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;div class="btn-group"&gt;
            &lt;button class="btn btn-outline-primary btn-sm"
            @click="openModal(false, item)"&gt;編輯&lt;/button&gt;
            &lt;button class="btn btn-outline-danger btn-sm"
            @click="openDelProductModal(item)"&gt;刪除&lt;/button&gt;
          &lt;/div&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;ProductModal ref="productModal"
  :product="tempProduct"
  @update-product="updateProduct"&gt;&lt;/ProductModal&gt;
  &lt;DelModal ref="delModal"
  :item="tempProduct"
  @del-item="delProduct"&gt;&lt;/DelModal&gt;
&lt;/template&gt;

&lt;script&gt;
// 載入 ProductModal
import ProductModal from '@/components/ProductModal.vue';
import DelModal from '@/components/DelModal.vue';

export default {
  data() {
    return {
      // 產品資訊
      products: &#91;],
      // 分頁資訊
      pagination: {},
      tempProduct: {},
      // 目前狀態
      isNew: false,
      // 讀取狀態
      isLoading: false,
    };
  },
  // inject
  inject: &#91;'emitter'],
  // 定義 components 進行區域註冊
  components: {
    ProductModal,
    DelModal,
  },
  methods: {
    // 取得產品列表是多數的產品資訊
    getProducts() {
      const api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/products`;
      // 讀取效果
      this.isLoading = true;
      this.$http.get(api)
        .then((res) =&gt; {
          this.isLoading = false;
          if (res.data.success) {
            console.log(res.data);
            this.products = res.data.products;
            this.pagination = res.data.pagination;
          }
        });
    },
    openModal(isNew, item) {
      // console.log(isNew, item);
      // 流程判斷
      if (isNew) {
        this.tempProduct = {};
      } else {
        // 使用展開的形式取出資料
        this.tempProduct = { ...item };
      }
      this.isNew = isNew;
      const productComponent = this.$refs.productModal;
      productComponent.showModal();
    },
    updateProduct(item) {
      // 測試 tempProduct 參數有沒有正確傳過來
      // console.log(item);
      this.tempProduct = item;
      // 新增
      let api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/product`;
      let httpMethod = 'post';
      // 編輯
      if (!this.isNew) {
        api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/product/${item.id}`;
        httpMethod = 'put';
      }
      const productComponent = this.$refs.productModal;
      this.$http&#91;httpMethod](api, { data: this.tempProduct })
        .then((response) =&gt; {
          console.log(response);
          // modal 關閉
          productComponent.hideModal();
          // 判斷狀態推送不同的訊息內容
          if (response.data.success) {
            this.getProducts();
            this.emitter.emit('push-message', {
              style: 'success',
              title: '更新成功',
            });
          } else {
            this.emitter.emit('push-message', {
              style: 'danger',
              title: '更新失敗',
              content: response.data.message.join('、'),
            });
          }
        });
    },
    // 開啟刪除 Modal
    openDelProductModal(item) {
      this.tempProduct = { ...item };
      const delComponent = this.$refs.delModal;
      delComponent.showModal();
    },
    delProduct() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/product/${this.tempProduct.id}`;
      this.$http.delete(url).then((response) =&gt; {
        console.log(response.data);
        const delComponent = this.$refs.delModal;
        delComponent.hideModal();
        this.getProducts();
      });
    },
  },
  created() {
    this.getProducts();
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/components/ToastMessages.vue - 彈出訊息列表元件
&lt;template&gt;
  &lt;div class="toast-container position-absolute pe-3 top-0 end-0"&gt;
    &lt;Toast v-for="(msg, key) in messages" :key="key"
      :msg="msg"
    /&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import Toast from '@/components/Toast.vue';

export default {
  components: { Toast },
  data() {
    return {
      messages: &#91;],
    };
  },
  inject: &#91;'emitter'],
  mounted() {
    // 請自行補上 emitter 事件
    this.emitter.on('push-message', (message) =&gt; {
      const { style = 'success', title, content } = message;
      this.messages.push({ style, title, content });
    });
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/components/Toast.vue - 吐司元件
&lt;template&gt;
  &lt;div class="toast" role="alert" aria-live="assertive" aria-atomic="true" ref="toast"&gt;
    &lt;div class="toast-header"&gt;
      &lt;span :class="`bg-${msg.style}`" class="p-2 rounded me-2 d-inline-block"&gt;&lt;/span&gt;
      &lt;strong class="me-auto"&gt;{{ msg.title }}&lt;/strong&gt;
      &lt;button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"&gt;&lt;/button&gt;
    &lt;/div&gt;
    &lt;div class="toast-body" v-if="msg.content"&gt;
      {{ msg.content }}
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
import Toast from 'bootstrap/js/dist/toast';

export default {
  name: 'Toast',
  props: &#91;
    'msg',
  ],
  mounted() {
    const toastEl = this.$refs.toast;
    const toast = new Toast(toastEl, {
      delay: 6000,
    });
    toast.show();
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">加入分頁切換</h3>



<ol class="wp-block-list">
<li>分頁會使用元件的方式製作<br>在之後的訂單、優惠券也可使用分頁的功能</li>



<li>關於分頁是怎麼運作<br>透過 API 形式進行頁面的切換<br>查看管理控制台 [需驗證] 文件 &gt; 取得商品列表</li>



<li>示範直接透過分頁的參數切換頁面<br>在 Products.vue 檔案 getProduct() 直接加入參數切換</li>



<li>在 getProduct() 加上參數預設值的形式 page = 1<br>api 路徑就可以把變數載進來<br>查看 Console 顯示的訊息 pagination、products<br>將後端傳來製作分頁所需要的重要資訊製作成元件</li>



<li>新增 Pagination.vue 檔案和撰寫分頁元件程式碼<br>關於指令、JS 自己練習撰寫</li>



<li>在 Products.vue 檔案加入 &lt;Pagination> 標籤<br>在外層定義 Products 元件，在內層定義 Pagination 元件，把分頁所需要的資訊傳進去，使用 props 的形式把分頁的資訊傳進去 props:pagination，就是從 AJAX 取回來的相關資訊。在點擊分頁的時候會觸發 emit，執行頁面切換使用，會回傳 emits 觸發 getProduct 事件、同時把頁碼帶進來，透過這種方式進行頁面的切換</li>



<li>在 Pagination.vue 檔案預期外面會傳入一個 pages，這個就是 pagination，刻意用不同的名稱來代表，在這裡會傳入一個 pagination，是從 AJAX 所取回來的資料</li>



<li>在 Products.vue 檔案<br>在產品頁面的 getProducts 之後是有把 pagination 的資訊存起來，存起來的內容可以在 &lt;Pagination&gt; 標籤帶進來，重點: 前內後外的概念，前面放入內層所需要接收的資訊，外層帶入 pagination</li>



<li>在 Pagination 檔案把頁碼加到這個區塊<br>在 AJAX 所取回來的資料 pagination 裡面有 total_pages 變數，就可以把變數帶進來，使用 v-for 的形式、page in pages 從外層帶進來的 props 名稱 pages 然後找到 total_pages，使用 v-for 要再加上 key，帶入的是 page 名稱，在 &lt;a&gt; 連結把 page 帶進來</li>



<li>預期點下按鈕時會切換頁面<br>使用 @click.prevent 加上一個自定義事件的名稱 updatePage 並且把頁碼帶進去<br>調整一下函式的內容，會直接對外發送切換頁面的事件<br>this.$emit(’emit-pages’, page)</li>



<li>在 Products.vue 檔案做主要的切換，預期會透過 emit 事件把事件往外送，在 &lt;Pagination&gt; 標籤加上由前內後外，內層事件名稱，預期切換 Products、觸發 getProducts 的事件，外層 getProducts 事件</li>



<li>在 Pagination.vue 檔案加上 active 樣式判斷</li>



<li>上一頁、下一頁自行製作</li>
</ol>



<pre class="wp-block-code"><code>// src/components/Pagination.vue
&lt;template&gt;
  &lt;nav aria-label="Page navigation example"&gt;
    &lt;ul class="pagination justify-content-center"&gt;
      &lt;li class="page-item" :class="{ 'disabled': !pages.has_pre }"&gt;
        &lt;a class="page-link" href="#" aria-label="Previous"
        @click.prevent="updatePage(pages.current_page - 1)"&gt;
          &lt;span aria-hidden="true"&gt;&amp;laquo;&lt;/span&gt;
        &lt;/a&gt;
      &lt;/li&gt;
      &lt;li class="page-item"
      v-for="page in pages.total_pages"
      :key="page"
      :class="{ 'active': page === pages.current_page }"&gt;
        &lt;a class="page-link" href="#"
        @click.prevent="updatePage(page)"&gt;
          {{ page }}
        &lt;/a&gt;
      &lt;/li&gt;
      &lt;li class="page-item" :class="{ 'disabled': !pages.has_next }"&gt;
        &lt;a class="page-link" href="#" aria-label="Next"
        @click.prevent="updatePage(pages.current_page + 1)"&gt;
          &lt;span aria-hidden="true"&gt;&amp;raquo;&lt;/span&gt;
        &lt;/a&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/nav&gt;
&lt;/template&gt;

&lt;script&gt;
// :pages="{ 頁碼資訊 }"
// @emitPages="更新頁面事件"
export default {
  props: &#91;'pages'],
  methods: {
    updatePage(page) {
      this.$emit('emit-pages', page);
    },
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// views/Products.vue
&lt;template&gt;
  &lt;Loading :active="isLoading"&gt;&lt;/Loading&gt;
  &lt;div class="text-end mt-3"&gt;
    &lt;button class="btn btn-primary" type="button"
    @click="openModal(true)"&gt;
      增加一個產品
    &lt;/button&gt;
  &lt;/div&gt;
  &lt;table class="table mt-4"&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th width="120"&gt;分類&lt;/th&gt;
        &lt;th&gt;產品名稱&lt;/th&gt;
        &lt;th width="120"&gt;原價&lt;/th&gt;
        &lt;th width="120"&gt;售價&lt;/th&gt;
        &lt;th width="100"&gt;是否啟用&lt;/th&gt;
        &lt;th width="200"&gt;編輯&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr v-for="item in products" :key="item.id"&gt;
        &lt;td&gt;{{ item.category }}&lt;/td&gt;
        &lt;td&gt;{{ item.title }}&lt;/td&gt;
        &lt;td class="text-right"&gt;
          {{ item.origin_price }}
        &lt;/td&gt;
        &lt;td class="text-right"&gt;
          {{ item.price }}
        &lt;/td&gt;
        &lt;td&gt;
          &lt;span class="text-success" v-if="item.is_enabled"&gt;啟用&lt;/span&gt;
          &lt;span class="text-muted" v-else&gt;未啟用&lt;/span&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;div class="btn-group"&gt;
            &lt;button class="btn btn-outline-primary btn-sm"
            @click="openModal(false, item)"&gt;編輯&lt;/button&gt;
            &lt;button class="btn btn-outline-danger btn-sm"
            @click="openDelProductModal(item)"&gt;刪除&lt;/button&gt;
          &lt;/div&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;Pagination :pages="pagination"
  @emit-pages="getProducts"&gt;&lt;/Pagination&gt;
  &lt;ProductModal ref="productModal"
  :product="tempProduct"
  @update-product="updateProduct"&gt;&lt;/ProductModal&gt;
  &lt;DelModal ref="delModal"
  :item="tempProduct"
  @del-item="delProduct"&gt;&lt;/DelModal&gt;
&lt;/template&gt;

&lt;script&gt;
// 載入 ProductModal
import ProductModal from '@/components/ProductModal.vue';
import DelModal from '@/components/DelModal.vue';
import Pagination from '@/components/Pagination.vue';

export default {
  data() {
    return {
      // 產品資訊
      products: &#91;],
      // 分頁資訊
      pagination: {},
      tempProduct: {},
      // 目前狀態
      isNew: false,
      // 讀取狀態
      isLoading: false,
    };
  },
  // inject
  inject: &#91;'emitter'],
  // 定義 components 進行區域註冊
  components: {
    ProductModal,
    DelModal,
    Pagination,
  },
  methods: {
    // 取得產品列表是多數的產品資訊
    getProducts(page = 1) {
      const api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/products?page=${page}`;
      // 讀取效果
      this.isLoading = true;
      this.$http.get(api)
        .then((res) =&gt; {
          this.isLoading = false;
          if (res.data.success) {
            console.log(res.data);
            this.products = res.data.products;
            this.pagination = res.data.pagination;
          }
        });
    },
    openModal(isNew, item) {
      // console.log(isNew, item);
      // 流程判斷
      if (isNew) {
        this.tempProduct = {};
      } else {
        // 使用展開的形式取出資料
        this.tempProduct = { ...item };
      }
      this.isNew = isNew;
      const productComponent = this.$refs.productModal;
      productComponent.showModal();
    },
    updateProduct(item) {
      // 測試 tempProduct 參數有沒有正確傳過來
      // console.log(item);
      this.tempProduct = item;
      // 新增
      let api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/product`;
      let httpMethod = 'post';
      // 編輯
      if (!this.isNew) {
        api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/product/${item.id}`;
        httpMethod = 'put';
      }
      const productComponent = this.$refs.productModal;
      this.$http&#91;httpMethod](api, { data: this.tempProduct })
        .then((response) =&gt; {
          console.log(response);
          // modal 關閉
          productComponent.hideModal();
          // 判斷狀態推送不同的訊息內容
          if (response.data.success) {
            this.getProducts();
            this.emitter.emit('push-message', {
              style: 'success',
              title: '更新成功',
            });
          } else {
            this.emitter.emit('push-message', {
              style: 'danger',
              title: '更新失敗',
              content: response.data.message.join('、'),
            });
          }
        });
    },
    // 開啟刪除 Modal
    openDelProductModal(item) {
      this.tempProduct = { ...item };
      const delComponent = this.$refs.delModal;
      delComponent.showModal();
    },
    delProduct() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/product/${this.tempProduct.id}`;
      this.$http.delete(url).then((response) =&gt; {
        console.log(response.data);
        const delComponent = this.$refs.delModal;
        delComponent.hideModal();
        this.getProducts();
      });
    },
  },
  created() {
    this.getProducts();
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">套用全域的千分號</h3>



<ol class="wp-block-list">
<li>數值超過一千以上建議加上千分號，在閱讀上面會比較好，在產品頁面、訂單、優惠券…等都有可能會加入千分號，會建議把千分號功能獨立起來，在每一個頁面、元件都可以使用</li>



<li>新增 filters.js 檔案<br>加入千分號方法、轉換時間格式方法</li>



<li>在 Products.vue 檔案有 origin_price、price<br>把函式加入轉換成千分號的形式<br>匯入 filters.js 檔案，在 Products.vue 檔案只需要匯入 currency 方法<br>將 currency 加到 methods 方法裡面就可以直接使用<br>currency 加在金額的前方，因為 currency 本身是函式必須使用括號</li>



<li>額外的技巧，在 Vue3 官方文件有應用配置 &gt; globalProperties，是全域的屬性<br>可以使用 app.config.globalProperties 定義一個全域的屬性的方法，自定義任何想加入的屬性<br>在每個子元件下都可以直接去使用 this 方式呼叫這個變數，如果是加入一般的純值效益其實不大，加入方法的話，就像千分號方法使用度就會高很多</li>



<li>實作額外的技巧<br>在 main.js 檔案就可以把 filters.js 方法匯入<br>參考官方文件 app.config 設定<br>app.config.globalProperties 然後加上一個自定義的屬性名稱，使用 $filters 這個名稱作為一個集合，建議在屬性名稱最前方加上$，這樣比較不會跟區域元件裡面的變數產生衝突，等於一個物件，就可以把 currency 方法帶進來</li>



<li>filters.js 其實有很多方法，可以陸續加入許多方法，都可以透過這種方式加到 filters 物件裡面</li>



<li>在 Products.vue 檔案，原本是直接呼叫 currency 方法，現在會在前面加上 $filters，可自定義屬性名稱</li>



<li>使用 $filters 這種方式加到全域的屬性，就必須把原本在區域的方式把移除</li>
</ol>



<pre class="wp-block-code"><code>// methods/filters.js
export function currency(num) {
  const n = parseInt(num, 10);
  return `${n.toFixed(0).replace(/./g, (c, i, a) =&gt; (i &amp;&amp; c !== '.' &amp;&amp; ((a.length - i) % 3 === 0) ? `, ${c}`.replace(/\s/g, '') : c))}`;
}

export function date(time) {
  const localDate = new Date(time * 1000);
  return localDate.toLocaleDateString();
}
</code></pre>



<pre class="wp-block-code"><code>// views/Products.vue
&lt;template&gt;
  &lt;Loading :active="isLoading"&gt;&lt;/Loading&gt;
  &lt;div class="text-end mt-3"&gt;
    &lt;button class="btn btn-primary" type="button"
    @click="openModal(true)"&gt;
      增加一個產品
    &lt;/button&gt;
  &lt;/div&gt;
  &lt;table class="table mt-4"&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th width="120"&gt;分類&lt;/th&gt;
        &lt;th&gt;產品名稱&lt;/th&gt;
        &lt;th width="120"&gt;原價&lt;/th&gt;
        &lt;th width="120"&gt;售價&lt;/th&gt;
        &lt;th width="100"&gt;是否啟用&lt;/th&gt;
        &lt;th width="200"&gt;編輯&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr v-for="item in products" :key="item.id"&gt;
        &lt;td&gt;{{ item.category }}&lt;/td&gt;
        &lt;td&gt;{{ item.title }}&lt;/td&gt;
        &lt;td class="text-right"&gt;
          {{ $filters.currency(item.origin_price) }}
        &lt;/td&gt;
        &lt;td class="text-right"&gt;
          {{ $filters.currency(item.price) }}
        &lt;/td&gt;
        &lt;td&gt;
          &lt;span class="text-success" v-if="item.is_enabled"&gt;啟用&lt;/span&gt;
          &lt;span class="text-muted" v-else&gt;未啟用&lt;/span&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;div class="btn-group"&gt;
            &lt;button class="btn btn-outline-primary btn-sm"
            @click="openModal(false, item)"&gt;編輯&lt;/button&gt;
            &lt;button class="btn btn-outline-danger btn-sm"
            @click="openDelProductModal(item)"&gt;刪除&lt;/button&gt;
          &lt;/div&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;Pagination :pages="pagination"
  @emit-pages="getProducts"&gt;&lt;/Pagination&gt;
  &lt;ProductModal ref="productModal"
  :product="tempProduct"
  @update-product="updateProduct"&gt;&lt;/ProductModal&gt;
  &lt;DelModal ref="delModal"
  :item="tempProduct"
  @del-item="delProduct"&gt;&lt;/DelModal&gt;
&lt;/template&gt;

&lt;script&gt;
// 載入 ProductModal
import ProductModal from '@/components/ProductModal.vue';
import DelModal from '@/components/DelModal.vue';
import Pagination from '@/components/Pagination.vue';
// import { currency } from '@/methods/filters';

export default {
  data() {
    return {
      // 產品資訊
      products: &#91;],
      // 分頁資訊
      pagination: {},
      tempProduct: {},
      // 目前狀態
      isNew: false,
      // 讀取狀態
      isLoading: false,
    };
  },
  // inject
  inject: &#91;'emitter'],
  // 定義 components 進行區域註冊
  components: {
    ProductModal,
    DelModal,
    Pagination,
  },
  methods: {
    // 取得產品列表是多數的產品資訊
    getProducts(page = 1) {
      const api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/products?page=${page}`;
      // 讀取效果
      this.isLoading = true;
      this.$http.get(api)
        .then((res) =&gt; {
          this.isLoading = false;
          if (res.data.success) {
            console.log(res.data);
            this.products = res.data.products;
            this.pagination = res.data.pagination;
          }
        });
    },
    openModal(isNew, item) {
      // console.log(isNew, item);
      // 流程判斷
      if (isNew) {
        this.tempProduct = {};
      } else {
        // 使用展開的形式取出資料
        this.tempProduct = { ...item };
      }
      this.isNew = isNew;
      const productComponent = this.$refs.productModal;
      productComponent.showModal();
    },
    updateProduct(item) {
      // 測試 tempProduct 參數有沒有正確傳過來
      // console.log(item);
      this.tempProduct = item;
      // 新增
      let api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/product`;
      let httpMethod = 'post';
      // 編輯
      if (!this.isNew) {
        api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/product/${item.id}`;
        httpMethod = 'put';
      }
      const productComponent = this.$refs.productModal;
      this.$http&#91;httpMethod](api, { data: this.tempProduct })
        .then((response) =&gt; {
          console.log(response);
          // modal 關閉
          productComponent.hideModal();
          // 判斷狀態推送不同的訊息內容
          if (response.data.success) {
            this.getProducts();
            this.emitter.emit('push-message', {
              style: 'success',
              title: '更新成功',
            });
          } else {
            this.emitter.emit('push-message', {
              style: 'danger',
              title: '更新失敗',
              content: response.data.message.join('、'),
            });
          }
        });
    },
    // 開啟刪除 Modal
    openDelProductModal(item) {
      this.tempProduct = { ...item };
      const delComponent = this.$refs.delModal;
      delComponent.showModal();
    },
    delProduct() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/product/${this.tempProduct.id}`;
      this.$http.delete(url).then((response) =&gt; {
        console.log(response.data);
        const delComponent = this.$refs.delModal;
        delComponent.hideModal();
        this.getProducts();
      });
    },
    // currency,
  },
  created() {
    this.getProducts();
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/main.js
import { createApp } from 'vue';
import axios from 'axios';
import VueAxios from 'vue-axios';
import Loading from 'vue3-loading-overlay';
import 'vue3-loading-overlay/dist/vue3-loading-overlay.css';
import App from './App.vue';
import router from './router';
import { currency } from './methods/filters';

const app = createApp(App);
app.config.globalProperties.$filters = {
  currency,
};
app.use(VueAxios, axios);
app.use(router);
app.component('Loading', Loading);
app.mount('#app');
</code></pre>



<h3 class="wp-block-heading">中場加速</h3>



<ol class="wp-block-list">
<li>完成訂單、優惠券的頁面</li>



<li>訂單 – 訂單列表、編輯訂單、刪除訂單、分頁功能</li>



<li>在 Products.vue 檔案<br>在每次發送 http 行為的時候，都會針對事件成功與否觸發 emitter，emitter 就會把成功與否透過吐司的方式呈現，封裝方法就放在 pushMessageState.js</li>



<li>在 main.js 檔案加入全域的屬性<br>app.config.globalProperties.$httpMessageState = $httpMessageState;<br>加入後就可以在每個程式碼都可以直接呼叫這個方法<br>注意: 這個方法是整合 AJAX 的一些錯誤事件，統一整理發送給 Toast 使用，正常來說不太建議把太多的方法掛在全域下面，會不知道這個方法來自於哪裡<br>可以使用 provide 來處理</li>



<li>如何透過 API 完成一個作品<br>下一個章節介紹客戶購物的部分<br>如何完成查看特定產品、以及加到購物車的內容裡面<br>選擇產品的圖片、撰寫產品的文案，並且把內容加到自行設計的介面<br>如何製作購物車、包含購物車列表以及表單填寫的部分</li>
</ol>



<pre class="wp-block-code"><code>// src/components/CouponModal.vue
&lt;template&gt;
  &lt;div class="modal fade" id="couponModal" tabindex="-1" role="dialog"
  aria-labelledby="exampleModalLabel" aria-hidden="true" ref="modal"&gt;
    &lt;div class="modal-dialog" role="document"&gt;
      &lt;div class="modal-content"&gt;
        &lt;div class="modal-header"&gt;
          &lt;h5 class="modal-title" id="exampleModalLabel"&gt;Modal title&lt;/h5&gt;
          &lt;button type="button" class="btn-close"
          data-bs-dismiss="modal" aria-label="Close"&gt;&lt;/button&gt;
        &lt;/div&gt;
        &lt;div class="modal-body"&gt;
          &lt;div class="mb-3"&gt;
            &lt;label for="title"&gt;標題&lt;/label&gt;
            &lt;input type="text" class="form-control" id="title" v-model="tempCoupon.title"
            placeholder="請輸入標題"&gt;
          &lt;/div&gt;
          &lt;div class="mb-3"&gt;
            &lt;label for="coupon_code"&gt;優惠碼&lt;/label&gt;
            &lt;input type="text" class="form-control" id="coupon_code" v-model="tempCoupon.code"
            placeholder="請輸入優惠碼"&gt;
          &lt;/div&gt;
          &lt;div class="mb-3"&gt;
            &lt;label for="due_date"&gt;到期日&lt;/label&gt;
            &lt;input type="date" class="form-control" id="due_date"
            v-model="due_date"&gt;
          &lt;/div&gt;
          &lt;div class="mb-3"&gt;
            &lt;label for="price"&gt;折扣百分比&lt;/label&gt;
            &lt;input type="number" class="form-control" id="price"
            v-model.number="tempCoupon.percent" placeholder="請輸入折扣百分比"&gt;
          &lt;/div&gt;
          &lt;div class="mb-3"&gt;
            &lt;div class="form-check"&gt;
              &lt;input class="form-check-input" type="checkbox"
              :true-value="1"
              :false-value="0"
              v-model="tempCoupon.is_enabled" id="is_enabled"&gt;
              &lt;label class="form-check-label" for="is_enabled"&gt;
                是否啟用
              &lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="modal-footer"&gt;
          &lt;button type="button" class="btn btn-secondary" data-bs-dismiss="modal"&gt;Close&lt;/button&gt;
          &lt;button type="button" class="btn btn-primary"
                  @click="$emit('update-coupon', tempCoupon)"&gt;更新優惠券
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
import modalMixin from '@/mixins/modalMixin';

export default {
  name: 'couponModal',
  props: {
    coupon: {},
  },
  data() {
    return {
      tempCoupon: {},
      due_date: '',
    };
  },
  emits: &#91;'update-coupon'],
  watch: {
    coupon() {
      this.tempCoupon = this.coupon;
      // 將時間格式改為 YYYY-MM-DD
      console.log(this.tempCoupon.due_date);
      const dateAndTime = new Date(this.tempCoupon.due_date * 1000)
        .toISOString().split('T');
      &#91;this.due_date] = dateAndTime;
    },
    due_date() {
      this.tempCoupon.due_date = Math.floor(new Date(this.due_date) / 1000);
    },
  },
  mixins: &#91;modalMixin],
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/components/Navbar.vue
&lt;template&gt;
  &lt;nav class="navbar navbar-expand-lg bg-body-tertiary"&gt;
    &lt;div class="container-fluid"&gt;
      &lt;a class="navbar-brand" href="#"&gt;購物趣&lt;/a&gt;
      &lt;button class="navbar-toggler" type="button"
      data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup"
      aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation"&gt;
        &lt;span class="navbar-toggler-icon"&gt;&lt;/span&gt;
      &lt;/button&gt;
      &lt;div class="collapse navbar-collapse" id="navbarNavAltMarkup"&gt;
        &lt;ul class="navbar-nav me-auto mb-2 mb-lg-0"&gt;
          &lt;li class="nav-item"&gt;
            &lt;router-link to="/dashboard/products" class="nav-link"&gt;產品&lt;/router-link&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;router-link to="/dashboard/orders" class="nav-link"&gt;訂單&lt;/router-link&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;router-link to="/dashboard/coupons" class="nav-link"&gt;優惠券&lt;/router-link&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#" @click.prevent="logout"&gt;登出&lt;/a&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
        &lt;span class="navbar-text"&gt;
          後台管理
        &lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  methods: {
    logout() {
      const api = `${process.env.VUE_APP_API}logout`;
      this.$http.post(api, this.user)
        .then((res) =&gt; {
          if (res.data.success) {
            // console.log(res);
            this.$router.push('/login');
          }
        });
    },
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/components/OrderModal.vue
&lt;template&gt;
  &lt;div class="modal fade" id="productModal" tabindex="-1" role="dialog"
  aria-labelledby="exampleModalLabel" aria-hidden="true" ref="modal"&gt;
    &lt;div class="modal-dialog modal-xl" role="document"&gt;
      &lt;div class="modal-content border-0"&gt;
        &lt;div class="modal-header bg-dark text-white"&gt;
          &lt;h5 class="modal-title" id="exampleModalLabel"&gt;
            &lt;span&gt;訂單細節&lt;/span&gt;
          &lt;/h5&gt;
          &lt;button type="button" class="btn-close bg-light"
                  data-bs-dismiss="modal" aria-label="Close"&gt;&lt;/button&gt;
        &lt;/div&gt;
        &lt;div class="modal-body"&gt;
          &lt;div class="row"&gt;
            &lt;div class="col-md-4"&gt;
              &lt;h3&gt;用戶資料&lt;/h3&gt;
              &lt;table class="table"&gt;
                &lt;tbody v-if="tempOrder.user"&gt;
                  &lt;tr&gt;
                    &lt;th style="width: 100px;"&gt;姓名&lt;/th&gt;
                    &lt;td&gt;{{ tempOrder.user.name }}&lt;/td&gt;
                  &lt;/tr&gt;
                  &lt;tr&gt;
                    &lt;th&gt;Email&lt;/th&gt;
                    &lt;td&gt;{{ tempOrder.user.email }}&lt;/td&gt;
                  &lt;/tr&gt;
                  &lt;tr&gt;
                    &lt;th&gt;電話&lt;/th&gt;
                    &lt;td&gt;{{ tempOrder.user.tel }}&lt;/td&gt;
                  &lt;/tr&gt;
                  &lt;tr&gt;
                    &lt;th&gt;地址&lt;/th&gt;
                    &lt;td&gt;{{ tempOrder.user.address }}&lt;/td&gt;
                  &lt;/tr&gt;
                &lt;/tbody&gt;
              &lt;/table&gt;
            &lt;/div&gt;
            &lt;div class="col-md-8"&gt;
              &lt;h3&gt;訂單細節&lt;/h3&gt;
              &lt;table class="table"&gt;
                &lt;tbody&gt;
                  &lt;tr&gt;
                    &lt;th style="width: 100px"&gt;訂單編號&lt;/th&gt;
                    &lt;td&gt;{{ tempOrder.id }}&lt;/td&gt;
                  &lt;/tr&gt;
                  &lt;tr&gt;
                    &lt;th&gt;下單時間&lt;/th&gt;
                    &lt;td&gt;{{ $filters.date(tempOrder.create_at)}}&lt;/td&gt;
                  &lt;/tr&gt;
                  &lt;tr&gt;
                    &lt;th&gt;付款時間&lt;/th&gt;
                    &lt;td&gt;
                      &lt;span v-if="tempOrder.paid_date"&gt;
                        {{ $filters.date(tempOrder.paid_date) }}
                      &lt;/span&gt;
                      &lt;span v-else&gt;時間不正確&lt;/span&gt;
                    &lt;/td&gt;
                  &lt;/tr&gt;
                  &lt;tr&gt;
                    &lt;th&gt;付款狀態&lt;/th&gt;
                    &lt;td&gt;
                      &lt;strong v-if="tempOrder.is_paid" class="text-success"&gt;已付款&lt;/strong&gt;
                      &lt;span v-else class="text-muted"&gt;尚未付款&lt;/span&gt;
                    &lt;/td&gt;
                  &lt;/tr&gt;
                  &lt;tr&gt;
                    &lt;th&gt;總金額&lt;/th&gt;
                    &lt;td&gt;
                      {{ $filters.currency(tempOrder.total) }}
                    &lt;/td&gt;
                  &lt;/tr&gt;
                &lt;/tbody&gt;
              &lt;/table&gt;
              &lt;h3&gt;選購商品&lt;/h3&gt;
              &lt;table class="table"&gt;
                &lt;thead&gt;
                  &lt;tr&gt;
                  &lt;/tr&gt;
                &lt;/thead&gt;
                &lt;tbody&gt;
                  &lt;tr v-for="item in tempOrder.products" :key="item.id"&gt;
                    &lt;th&gt;
                      {{ item.product.title }}
                    &lt;/th&gt;
                    &lt;td&gt;
                      {{ item.qty }} / {{ item.product.unit }}
                    &lt;/td&gt;
                    &lt;td class="text-end"&gt;
                      {{ $filters.currency(item.final_total) }}
                    &lt;/td&gt;
                  &lt;/tr&gt;
                &lt;/tbody&gt;
              &lt;/table&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="modal-footer"&gt;
          &lt;button type="button" class="btn btn-outline-secondary"
                  data-bs-dismiss="modal"&gt;取消
          &lt;/button&gt;
          &lt;button type="button" class="btn btn-primary"
                  @click="$emit('update-order', tempOrder)"&gt;確認&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
import modalMixin from '@/mixins/modalMixin';

export default {
  name: 'orderModal',
  props: {
    order: {
      type: Object,
      default() { return {}; },
    },
  },
  data() {
    return {
      status: {},
      modal: '',
      tempOrder: {},
      isPaid: false,
    };
  },
  emits: &#91;'update-product'],
  mixins: &#91;modalMixin],
  inject: &#91;'emitter'],
  watch: {
    order() {
      this.tempOrder = this.order;
      this.isPaid = this.tempOrder.is_paid;
    },
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/main.js
import { createApp } from 'vue';
import axios from 'axios';
import VueAxios from 'vue-axios';
import Loading from 'vue3-loading-overlay';
import 'vue3-loading-overlay/dist/vue3-loading-overlay.css';
import App from './App.vue';
import router from './router';
import { currency, date } from './methods/filters';
import $httpMessageState from './methods/pushMessageState';

const app = createApp(App);
app.config.globalProperties.$filters = {
  currency,
  date,
};
// 此函式的用途是整合 AJAX 的錯誤事件，統一整理發送給予 Toast 處理
app.config.globalProperties.$httpMessageState = $httpMessageState;

app.use(VueAxios, axios);
app.use(router);
app.component('Loading', Loading);
app.mount('#app');
</code></pre>



<pre class="wp-block-code"><code>// src/methods/pushMessageState.js
import emitter from '@/methods/emitter';

export default function (response, title = '更新') {
  if (response.data.success) {
    emitter.emit('push-message', {
      style: 'success',
      title: `${title}成功`,
    });
  } else {
    // 有些訊息是字串，有些則是陣列，在此統一格式
    const message = typeof response.data.message === 'string'
      ? &#91;response.data.message] : response.data.message;
    emitter.emit('push-message', {
      style: 'danger',
      title: `${title}失敗`,
      content: message.join('、'),
    });
  }
}
</code></pre>



<pre class="wp-block-code"><code>// src/router/index.js
import { createRouter, createWebHashHistory } from 'vue-router';
import Home from '../views/Home.vue';

const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/login',
    component: () =&gt; import('../views/Login.vue'),
  },
  {
    path: '/dashboard',
    component: () =&gt; import('../views/Dashboard.vue'),
    // 巢狀路由 - 產品列表
    children: &#91;
      {
        path: 'products',
        component: () =&gt; import('../views/Products.vue'),
      },
      {
        path: 'orders',
        component: () =&gt; import('../views/Orders.vue'),
      },
      {
        path: 'coupons',
        component: () =&gt; import('../views/Coupons.vue'),
      },
    ],
  },
];

const router = createRouter({
  history: createWebHashHistory(),
  routes,
  linkActiveClass: 'active',
});

export default router;
</code></pre>



<pre class="wp-block-code"><code>// src/views/Coupons.vue
&lt;template&gt;
  &lt;div&gt;
    &lt;Loading :active="isLoading"&gt;&lt;/Loading&gt;
    &lt;div class="text-end mt-4"&gt;
      &lt;button class="btn btn-primary" @click="openCouponModal(true)"&gt;
        建立新的優惠券
      &lt;/button&gt;
    &lt;/div&gt;
    &lt;table class="table mt-4"&gt;
      &lt;thead&gt;
      &lt;tr&gt;
        &lt;th&gt;名稱&lt;/th&gt;
        &lt;th&gt;折扣百分比&lt;/th&gt;
        &lt;th&gt;到期日&lt;/th&gt;
        &lt;th&gt;是否啟用&lt;/th&gt;
        &lt;th&gt;編輯&lt;/th&gt;
      &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
      &lt;tr v-for="(item, key) in coupons" :key="key"&gt;
        &lt;td&gt;{{ item.title }}&lt;/td&gt;
        &lt;td&gt;{{ item.percent }}%&lt;/td&gt;
        &lt;td&gt;{{ $filters.date(item.due_date) }}&lt;/td&gt;
        &lt;td&gt;
          &lt;span v-if="item.is_enabled === 1" class="text-success"&gt;啟用&lt;/span&gt;
          &lt;span v-else class="text-muted"&gt;未起用&lt;/span&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;div class="btn-group"&gt;
            &lt;button class="btn btn-outline-primary btn-sm"
                    @click="openCouponModal(false, item)"
            &gt;編輯&lt;/button&gt;
            &lt;button class="btn btn-outline-danger btn-sm"
                    @click="openDelCouponModal(item)"
            &gt;刪除&lt;/button&gt;
          &lt;/div&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
    &lt;CouponModal :coupon="tempCoupon" ref="couponModal"
    @update-coupon="updateCoupon"&gt;&lt;/CouponModal&gt;
    &lt;DelModal :item="tempCoupon" ref="delModal" @del-item="delCoupon"/&gt;
    &lt;Pagination :pages="pagination" @emit-pages="getCoupons"&gt;&lt;/Pagination&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import CouponModal from '@/components/CouponModal.vue';
import DelModal from '@/components/DelModal.vue';
import Pagination from '@/components/Pagination.vue';

export default {
  components: {
    CouponModal,
    DelModal,
    Pagination,
  },
  props: {
    config: Object,
  },
  data() {
    return {
      coupons: {},
      tempCoupon: {
        title: '',
        is_enabled: 0,
        percent: 100,
        code: '',
      },
      isLoading: false,
      isNew: false,
      pagination: {},
      currentPage: 1,
    };
  },
  methods: {
    openCouponModal(isNew, item) {
      this.isNew = isNew;
      if (this.isNew) {
        this.tempCoupon = {
          due_date: new Date().getTime() / 1000,
        };
      } else {
        this.tempCoupon = { ...item };
      }
      this.$refs.couponModal.showModal();
    },
    openDelCouponModal(item) {
      this.tempCoupon = { ...item };
      const delComponent = this.$refs.delModal;
      delComponent.showModal();
    },
    getCoupons(currentPage = 1) {
      this.currentPage = currentPage;
      this.isLoading = true;
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/coupons?page=${currentPage}`;
      this.$http.get(url, this.tempProduct).then((response) =&gt; {
        this.coupons = response.data.coupons;
        this.pagination = response.data.pagination;
        this.isLoading = false;
        console.log(response);
      });
    },
    updateCoupon(tempCoupon) {
      if (this.isNew) {
        const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/coupon`;
        this.$http.post(url, { data: tempCoupon }).then((response) =&gt; {
          console.log(response, tempCoupon);
          this.$httpMessageState(response, '新增優惠券');
          this.getCoupons();
          this.$refs.couponModal.hideModal();
        });
      } else {
        const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/coupon/${this.tempCoupon.id}`;
        this.$http.put(url, { data: this.tempCoupon }).then((response) =&gt; {
          console.log(response);
          this.$httpMessageState(response, '新增優惠券');
          this.getCoupons();
          this.$refs.couponModal.hideModal();
        });
      }
    },
    delCoupon() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/coupon/${this.tempCoupon.id}`;
      this.isLoading = true;
      this.$http.delete(url).then((response) =&gt; {
        console.log(response, this.tempCoupon);
        this.$httpMessageState(response, '刪除優惠券');
        const delComponent = this.$refs.delModal;
        delComponent.hideModal();
        this.getCoupons();
      });
    },
  },
  created() {
    this.getCoupons();
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/views/Orders.vue
&lt;template&gt;
  &lt;Loading :active="isLoading"&gt;&lt;/Loading&gt;
  &lt;table class="table mt-4"&gt;
    &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;購買時間&lt;/th&gt;
      &lt;th&gt;Email&lt;/th&gt;
      &lt;th&gt;購買款項&lt;/th&gt;
      &lt;th&gt;應付金額&lt;/th&gt;
      &lt;th&gt;是否付款&lt;/th&gt;
      &lt;th&gt;編輯&lt;/th&gt;
    &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;template v-for="(item, key) in orders" :key="key"&gt;
        &lt;tr v-if="orders.length"
            :class="{'text-secondary': !item.is_paid}"&gt;
          &lt;td&gt;{{ $filters.date(item.create_at) }}&lt;/td&gt;
          &lt;td&gt;&lt;span v-text="item.user.email" v-if="item.user"&gt;&lt;/span&gt;&lt;/td&gt;
          &lt;td&gt;
            &lt;ul class="list-unstyled"&gt;
              &lt;li v-for="(product, i) in item.products" :key="i"&gt;
                {{ product.product.title }} 數量：{{ product.qty }}
                {{ product.product.unit }}
              &lt;/li&gt;
            &lt;/ul&gt;
          &lt;/td&gt;
          &lt;td class="text-right"&gt;{{ $filters.currency(item.total) }}&lt;/td&gt;
          &lt;td&gt;
            &lt;div class="form-check form-switch"&gt;
              &lt;input class="form-check-input" type="checkbox" :id="`paidSwitch${item.id}`"
              v-model="item.is_paid"
              @change="updatePaid(item)"&gt;
              &lt;label class="form-check-label" :for="`paidSwitch${item.id}`"&gt;
                &lt;span v-if="item.is_paid"&gt;已付款&lt;/span&gt;
                &lt;span v-else&gt;未付款&lt;/span&gt;
              &lt;/label&gt;
            &lt;/div&gt;
          &lt;/td&gt;
          &lt;td&gt;
            &lt;div class="btn-group"&gt;
              &lt;button class="btn btn-outline-primary btn-sm"
                      @click="openModal(false, item)"&gt;檢視&lt;/button&gt;
              &lt;button class="btn btn-outline-danger btn-sm"
                      @click="openDelOrderModal(item)"
              &gt;刪除&lt;/button&gt;
            &lt;/div&gt;
          &lt;/td&gt;
        &lt;/tr&gt;
      &lt;/template&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;OrderModal :order="tempOrder"
              ref="orderModal" @update-paid="updatePaid"&gt;&lt;/OrderModal&gt;
  &lt;DelModal :item="tempOrder" ref="delModal" @del-item="delOrder"&gt;&lt;/DelModal&gt;
  &lt;Pagination :pages="pagination" @emit-pages="getOrders"&gt;&lt;/Pagination&gt;
&lt;/template&gt;

&lt;script&gt;
import DelModal from '@/components/DelModal.vue';
import OrderModal from '@/components/OrderModal.vue';
import Pagination from '@/components/Pagination.vue';

export default {
  data() {
    return {
      orders: {},
      isNew: false,
      pagination: {},
      isLoading: false,
      tempOrder: {},
      currentPage: 1,
    };
  },
  components: {
    Pagination,
    DelModal,
    OrderModal,
  },
  methods: {
    getOrders(currentPage = 1) {
      this.currentPage = currentPage;
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/orders?page=${currentPage}`;
      this.isLoading = true;
      this.$http.get(url, this.tempProduct).then((response) =&gt; {
        this.orders = response.data.orders;
        this.pagination = response.data.pagination;
        this.isLoading = false;
        console.log(response);
      });
    },
    openModal(isNew, item) {
      this.tempOrder = { ...item };
      this.isNew = false;
      const orderComponent = this.$refs.orderModal;
      orderComponent.showModal();
    },
    openDelOrderModal(item) {
      this.tempOrder = { ...item };
      const delComponent = this.$refs.delModal;
      delComponent.showModal();
    },
    updatePaid(item) {
      this.isLoading = true;
      const api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/order/${item.id}`;
      const paid = {
        is_paid: item.is_paid,
      };
      this.$http.put(api, { data: paid }).then((response) =&gt; {
        this.isLoading = false;
        this.getOrders(this.currentPage);
        this.$httpMessageState(response, '更新付款狀態');
      });
    },
    delOrder() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/admin/order/${this.tempOrder.id}`;
      this.isLoading = true;
      this.$http.delete(url).then((response) =&gt; {
        console.log(response);
        const delComponent = this.$refs.delModal;
        delComponent.hideModal();
        this.getOrders(this.currentPage);
      });
    },
  },
  created() {
    this.getOrders();
    console.log(process.env.VUE_APP_API);
  },
};
&lt;/script&gt;
</code></pre>



<h4 class="wp-block-heading">練習</h4>



<ul class="wp-block-list">
<li>使用測試 Web API 應用程式 Postman、 Hoppscotch 建立優惠券和訂單</li>



<li>Vue3_API
<ul class="wp-block-list">
<li>POST 登入功能</li>



<li>POST 新增優惠券</li>



<li>GET 取得商品列表</li>



<li>POST 加入購物車</li>



<li>PUT 更新購物車</li>



<li>POST 結帳頁面</li>



<li>GET 取得訂單列表</li>
</ul>
</li>
</ul>



<h3 class="wp-block-heading">用戶端產品列表</h3>



<ol class="wp-block-list">
<li>製作客戶購物的部分<br>查看 API 文件 – 客戶購物 [免驗證]<br>購物流程: 取得商品列表、單一商品細節、加入購物車、刪除某一筆購物車資料、套用優惠券、結帳頁面、結帳付款…等</li>



<li>提醒: 點選任何一個產品的時候，請用單一頁面呈現</li>



<li>在 index.js 路由表加上用戶端路由<br>新增 user 路徑，不需要驗證流程會放在 user 路徑下</li>



<li>建立 Userboard.vue 檔案並加入程式碼<br>相較於 Dashboard 較簡單</li>



<li>建立 UserCart.vue 檔案並加入程式碼<br>在 user 路徑新增子路由 cart 路徑，對應的是購物車<br>建立 UserProduct.vue 檔案並加入程式碼<br>在 user 路徑新增子路由 product/:productId 路徑，對應的是單一產品頁面</li>



<li>講解 UserCart.vue 檔案<br>methods 方法的函式，getProducts() 取得商品列表、getProduct(id) 進入單一商品的頁面</li>



<li>試著把用戶端商品列表呈現</li>
</ol>



<pre class="wp-block-code"><code>// router/index.js
import { createRouter, createWebHashHistory } from 'vue-router';
import Home from '../views/Home.vue';

const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/login',
    component: () =&gt; import('../views/Login.vue'),
  },
  {
    path: '/dashboard',
    component: () =&gt; import('../views/Dashboard.vue'),
    // 巢狀路由 - 產品列表
    children: &#91;
      {
        path: 'products',
        component: () =&gt; import('../views/Products.vue'),
      },
      {
        path: 'orders',
        component: () =&gt; import('../views/Orders.vue'),
      },
      {
        path: 'coupons',
        component: () =&gt; import('../views/Coupons.vue'),
      },
    ],
  },
  {
    path: '/user',
    component: () =&gt; import('../views/Userboard.vue'),
    children: &#91;
      {
        path: 'cart',
        component: () =&gt; import('../views/UserCart.vue'),
      },
      {
        path: 'product/:productId',
        component: () =&gt; import('@/views/UserProduct.vue'),
      },
    ],
  },
];

const router = createRouter({
  history: createWebHashHistory(),
  routes,
  linkActiveClass: 'active',
});

export default router;
</code></pre>



<pre class="wp-block-code"><code>// views/Userboard.vue
&lt;template&gt;
  &lt;nav class="navbar navbar-expand-lg navbar-dark bg-dark"&gt;
    &lt;div class="container-fluid"&gt;
      &lt;router-link class="navbar-brand" to="/user/cart"&gt;購物趣&lt;/router-link&gt;
    &lt;/div&gt;
  &lt;/nav&gt;
  &lt;div class="container-fluid mt-3 position-relative"&gt;
    &lt;ToastMessages&gt;&lt;/ToastMessages&gt;
    &lt;router-view/&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import emitter from '@/methods/emitter';
import ToastMessages from '@/components/ToastMessages.vue';

export default {
  components: {
    ToastMessages,
  },
  provide() {
    return {
      emitter,
    };
  },
  created() {
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// views/UserCart.vue
&lt;template&gt;
  &lt;Loading :active="isLoading"&gt;&lt;/Loading&gt;
  &lt;div class="container"&gt;
    &lt;div class="row mt-4"&gt;
      &lt;div class="col-md-7"&gt;
        &lt;table class="table align-middle"&gt;
          &lt;thead&gt;
            &lt;tr&gt;
              &lt;th&gt;圖片&lt;/th&gt;
              &lt;th&gt;商品名稱&lt;/th&gt;
              &lt;th&gt;價格&lt;/th&gt;
              &lt;th&gt;&lt;/th&gt;
            &lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            &lt;tr v-for="item in products" :key="item.id"&gt;
              &lt;td style="width: 200px"&gt;
                &lt;div style="height: 100px; background-size: cover; background-position: center"
                :style="{backgroundImage: `url(${item.imageUrl})`}"&gt;&lt;/div&gt;
              &lt;/td&gt;
              &lt;td&gt;&lt;a href="#" class="text-dark"&gt;{{ item.title }}&lt;/a&gt;&lt;/td&gt;
              &lt;td&gt;
                &lt;div class="h5" v-if="!item.price"&gt;{{ item.origin_price }} 元&lt;/div&gt;
                &lt;del class="h6" v-if="item.price"&gt;原價 {{ item.origin_price }} 元&lt;/del&gt;
                &lt;div class="h5" v-if="item.price"&gt;現在只要 {{ item.price }} 元&lt;/div&gt;
              &lt;/td&gt;
              &lt;td&gt;
                &lt;div class="btn-group btn-group-sm"&gt;
                  &lt;button type="button" class="btn btn-outline-secondary"
                  @click="getProduct(item.id)"&gt;
                    查看更多
                  &lt;/button&gt;
                  &lt;button type="button" class="btn btn-outline-danger"&gt;
                    加到購物車
                  &lt;/button&gt;
                &lt;/div&gt;
              &lt;/td&gt;
            &lt;/tr&gt;
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;
      &lt;!-- 購物車列表 --&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      products: &#91;],
      product: {},
      status: {
        loadingItem: '',
      },
    };
  },
  methods: {
    // 取得商品列表
    getProducts() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/products/all`;
      this.isLoading = true;
      this.$http.get(url).then((response) =&gt; {
        this.products = response.data.products;
        console.log('products:', response);
        this.isLoading = false;
      });
    },
    // 進入單一商品頁面
    getProduct(id) {
      this.$router.push(`/user/product/${id}`);
    },
  },
  created() {
    this.getProducts();
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// views/UserProduct.vue
&lt;template&gt;
  &lt;Loading :active="isLoading"&gt;&lt;/Loading&gt;
  &lt;div class="container"&gt;
    &lt;nav aria-label="breadcrumb"&gt;
      &lt;ol class="breadcrumb"&gt;
        &lt;li class="breadcrumb-item"&gt;&lt;router-link to="/user/cart"&gt;購物車&lt;/router-link&gt;&lt;/li&gt;
        &lt;li class="breadcrumb-item active" aria-current="page"&gt;{{ product.title }}&lt;/li&gt;
      &lt;/ol&gt;
    &lt;/nav&gt;
    &lt;div class="row justify-content-center"&gt;
      &lt;article class="col-8"&gt;
        &lt;h2&gt;{{ product.title }}&lt;/h2&gt;
        &lt;div&gt;{{ product.content }}&lt;/div&gt;
        &lt;div&gt;{{ product.description }}&lt;/div&gt;
        &lt;img :src="product.imageUrl" alt="" class="img-fluid mb-3"&gt;
      &lt;/article&gt;
      &lt;div class="col-4"&gt;
        &lt;div class="h5" v-if="!product.price"&gt;{{ product.origin_price }} 元&lt;/div&gt;
        &lt;del class="h6" v-if="product.price"&gt;原價 {{ product.origin_price }} 元&lt;/del&gt;
        &lt;div class="h5" v-if="product.price"&gt;現在只要 {{ product.price }} 元&lt;/div&gt;
        &lt;hr&gt;
        &lt;button type="button" class="btn btn-outline-danger"
                @click="addToCart(product.id)"&gt;
          加到購物車
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      product: {},
      id: '',
    };
  },
  methods: {
    getProduct() {
      const api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/product/${this.id}`;
      this.isLoading = true;
      this.$http.get(api).then((response) =&gt; {
        console.log(response.data);
        this.isLoading = false;
        if (response.data.success) {
          this.product = response.data.product;
        }
      });
    },
    addToCart(id, qty = 1) {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/cart`;
      const cart = {
        product_id: id,
        qty,
      };
      this.isLoading = true;
      this.$http.post(url, { data: cart }).then((response) =&gt; {
        this.isLoading = false;
        this.$httpMessageState(response, '加入購物車');
        this.$router.push('/user/cart');
      });
    },
  },
  created() {
    this.id = this.$route.params.productId;
    this.getProduct();
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">用戶端加入購物車</h3>



<ol class="wp-block-list">
<li>撰寫加入購物車的行為 api</li>



<li>查看客戶購物 [免驗證] 文件 &gt; 加入購物車</li>



<li>在 UserCart.vue 檔案撰寫程式碼<br>使用 @click 方式觸發加入購物車事件、addCart 方法，把產品的 id 帶進來<br>在 JS methods 方法新增 addCart 方法，測試 id 有沒有正確的取出<br>載入 api、建立資料 product_id, qty、發送 api<br>資料參數格式有加入 data 屬性名稱</li>



<li>避免用戶重複點擊加入購物車，加入單一按鈕的讀取效果，當 loadingItem 為一個特定品項的時候按鈕會轉為 disabled 並加上讀取的狀態</li>



<li>調整 HTML 的部分加上<br>:disabled=”this.status.loadingItem === item.id”<br>這個屬性會因為 loadingItem 的品項跟當前 item.id 的品項相同的時候，就會把加入到購物車按鈕轉為 disabled 的狀態</li>



<li>針對點擊購物車後加上額外的讀取效果<br>在 Bootstrap 文件 &gt; Components &gt; Spinners，選擇放射狀的樣式加到程式碼，尺寸需要調整一下選擇最小的、調整色彩<br>當前品項加入判斷式，v-if=”this.status.loadingItem === item.id”<br>白色對比度不佳，轉換成紅色</li>



<li>用戶無法重複點擊，了解目前是讀取狀態<br>如何加入購物車、如何把特定的按鈕加上讀取的效果</li>



<li>試著加入購物車、特定頁面的加入購物車功能完成</li>
</ol>



<pre class="wp-block-code"><code>// views/UserCart.vue
&lt;template&gt;
  &lt;Loading :active="isLoading"&gt;&lt;/Loading&gt;
  &lt;div class="container"&gt;
    &lt;div class="row mt-4"&gt;
      &lt;div class="col-md-7"&gt;
        &lt;table class="table align-middle"&gt;
          &lt;thead&gt;
            &lt;tr&gt;
              &lt;th&gt;圖片&lt;/th&gt;
              &lt;th&gt;商品名稱&lt;/th&gt;
              &lt;th&gt;價格&lt;/th&gt;
              &lt;th&gt;&lt;/th&gt;
            &lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            &lt;tr v-for="item in products" :key="item.id"&gt;
              &lt;td style="width: 200px"&gt;
                &lt;div style="height: 100px; background-size: cover; background-position: center"
                :style="{backgroundImage: `url(${item.imageUrl})`}"&gt;&lt;/div&gt;
              &lt;/td&gt;
              &lt;td&gt;&lt;a href="#" class="text-dark"&gt;{{ item.title }}&lt;/a&gt;&lt;/td&gt;
              &lt;td&gt;
                &lt;div class="h5" v-if="!item.price"&gt;{{ item.origin_price }} 元&lt;/div&gt;
                &lt;del class="h6" v-if="item.price"&gt;原價 {{ item.origin_price }} 元&lt;/del&gt;
                &lt;div class="h5" v-if="item.price"&gt;現在只要 {{ item.price }} 元&lt;/div&gt;
              &lt;/td&gt;
              &lt;td&gt;
                &lt;div class="btn-group btn-group-sm"&gt;
                  &lt;button type="button" class="btn btn-outline-secondary"
                  @click="getProduct(item.id)"&gt;
                    查看更多
                  &lt;/button&gt;
                  &lt;button type="button" class="btn btn-outline-danger"
                  @click="addCart(item.id)"
                  :disabled="this.status.loadingItem === item.id"&gt;
                    &lt;div v-if="this.status.loadingItem === item.id"
                    class="spinner-grow text-danger spinner-grow-sm" role="status"&gt;
                      &lt;span class="visually-hidden"&gt;Loading...&lt;/span&gt;
                    &lt;/div&gt;
                    加到購物車
                  &lt;/button&gt;
                &lt;/div&gt;
              &lt;/td&gt;
            &lt;/tr&gt;
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;
      &lt;!-- 購物車列表 --&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      products: &#91;],
      product: {},
      // 讀取狀態
      status: {
        loadingItem: '', // 對應品項 id
      },
    };
  },
  methods: {
    // 取得商品列表
    getProducts() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/products/all`;
      this.isLoading = true;
      this.$http.get(url).then((response) =&gt; {
        this.products = response.data.products;
        console.log('products:', response);
        this.isLoading = false;
      });
    },
    // 進入單一商品頁面
    getProduct(id) {
      this.$router.push(`/user/product/${id}`);
    },
    // 加入購物車
    addCart(id) {
      // 測試 id 有沒有正確取出
      // console.log(id);
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/cart`;
      this.status.loadingItem = id;
      const cart = {
        product_id: id,
        qty: 1,
      };
      this.$http.post(url, { data: cart })
        .then((res) =&gt; {
          this.status.loadingItem = '';
          console.log(res);
        });
    },
  },
  created() {
    this.getProducts();
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// views/UserProduct.vue
&lt;template&gt;
  &lt;Loading :active="isLoading"&gt;&lt;/Loading&gt;
  &lt;div class="container"&gt;
    &lt;nav aria-label="breadcrumb"&gt;
      &lt;ol class="breadcrumb"&gt;
        &lt;li class="breadcrumb-item"&gt;&lt;router-link to="/user/cart"&gt;購物車&lt;/router-link&gt;&lt;/li&gt;
        &lt;li class="breadcrumb-item active" aria-current="page"&gt;{{ product.title }}&lt;/li&gt;
      &lt;/ol&gt;
    &lt;/nav&gt;
    &lt;div class="row justify-content-center"&gt;
      &lt;article class="col-8"&gt;
        &lt;h2&gt;{{ product.title }}&lt;/h2&gt;
        &lt;div&gt;{{ product.content }}&lt;/div&gt;
        &lt;div&gt;{{ product.description }}&lt;/div&gt;
        &lt;img :src="product.imageUrl" alt="" class="img-fluid mb-3"&gt;
      &lt;/article&gt;
      &lt;div class="col-4"&gt;
        &lt;div class="h5" v-if="!product.price"&gt;{{ product.origin_price }} 元&lt;/div&gt;
        &lt;del class="h6" v-if="product.price"&gt;原價 {{ product.origin_price }} 元&lt;/del&gt;
        &lt;div class="h5" v-if="product.price"&gt;現在只要 {{ product.price }} 元&lt;/div&gt;
        &lt;hr&gt;
        &lt;button type="button" class="btn btn-outline-danger"
                @click="addToCart(product.id)"&gt;
          加到購物車
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      product: {},
      id: '',
    };
  },
  methods: {
    getProduct() {
      const api = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/product/${this.id}`;
      this.isLoading = true;
      this.$http.get(api).then((response) =&gt; {
        console.log(response.data);
        this.isLoading = false;
        if (response.data.success) {
          this.product = response.data.product;
        }
      });
    },
    // 特定頁面加入購物車
    addToCart(id, qty = 1) {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/cart`;
      const cart = {
        product_id: id,
        qty,
      };
      this.isLoading = true;
      this.$http.post(url, { data: cart }).then((response) =&gt; {
        this.isLoading = false;
        this.$httpMessageState(response, '加入購物車');
        this.$router.push('/user/cart');
      });
    },
  },
  created() {
    this.id = this.$route.params.productId;
    this.getProduct();
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">用戶端加入 Bootstrap Icon</h3>



<ol class="wp-block-list">
<li>製作購物車列表</li>



<li>提醒: api 細節，把購物車 api 打，會發現這邊的結構和先前的不太一樣，這裡是 data 裡面還有 data，主要是因為這個 data 裡面有兩個結構，一個是購物車的品項，加入非常多的項目都會存在這個 carts 裡面</li>



<li>在畫面的部分可以看到非常多的項目<br>在 Console 查詢顯示的資料， carts、final_total、total<br>carts、final_total, total 這兩個項目需要存下來，可以存在一個物件裡面，或者拆成兩個來另外做儲存，都是必要出現在畫面上的項目</li>



<li>在 UserCart.vue 檔案撰寫 getCart() 方法<br>this.cart = response.data.data 所有的項目都存起來，這個 cart 就包含陣列的列表以及總金額<br>在購物車列表就使用 v-if 判斷陣列有沒有存在，如果存在的情況下就使用 v-for 把陣列的內容完整的呈現</li>



<li>數量調整的功能尚未製作</li>



<li>使用 icon 的形式替換刪除中文字<br>在 Bootstrap 文件找到 Icons<br>安裝 Bootstrap Icons 套件<br>把 Bootstrap Icons 載入到 main.js 檔案<br>import ‘bootstrap-icons/font/bootstrap-icons.css’;<br>把 icons 圖示加入</li>
</ol>



<pre class="wp-block-code"><code>// views/UserCart.vue
&lt;template&gt;
  &lt;Loading :active="isLoading"&gt;&lt;/Loading&gt;
  &lt;div class="container"&gt;
    &lt;div class="row mt-4"&gt;
      &lt;div class="col-md-7"&gt;
        &lt;table class="table align-middle"&gt;
          &lt;thead&gt;
            &lt;tr&gt;
              &lt;th&gt;圖片&lt;/th&gt;
              &lt;th&gt;商品名稱&lt;/th&gt;
              &lt;th&gt;價格&lt;/th&gt;
              &lt;th&gt;&lt;/th&gt;
            &lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            &lt;tr v-for="item in products" :key="item.id"&gt;
              &lt;td style="width: 200px"&gt;
                &lt;div style="height: 100px; background-size: cover; background-position: center"
                :style="{backgroundImage: `url(${item.imageUrl})`}"&gt;&lt;/div&gt;
              &lt;/td&gt;
              &lt;td&gt;&lt;a href="#" class="text-dark"&gt;{{ item.title }}&lt;/a&gt;&lt;/td&gt;
              &lt;td&gt;
                &lt;div class="h5" v-if="!item.price"&gt;{{ item.origin_price }} 元&lt;/div&gt;
                &lt;del class="h6" v-if="item.price"&gt;原價 {{ item.origin_price }} 元&lt;/del&gt;
                &lt;div class="h5" v-if="item.price"&gt;現在只要 {{ item.price }} 元&lt;/div&gt;
              &lt;/td&gt;
              &lt;td&gt;
                &lt;div class="btn-group btn-group-sm"&gt;
                  &lt;button type="button" class="btn btn-outline-secondary"
                  @click="getProduct(item.id)"&gt;
                    查看更多
                  &lt;/button&gt;
                  &lt;button type="button" class="btn btn-outline-danger"
                  @click="addCart(item.id)"
                  :disabled="this.status.loadingItem === item.id"&gt;
                    &lt;div v-if="this.status.loadingItem === item.id"
                    class="spinner-grow text-danger spinner-grow-sm" role="status"&gt;
                      &lt;span class="visually-hidden"&gt;Loading...&lt;/span&gt;
                    &lt;/div&gt;
                    加到購物車
                  &lt;/button&gt;
                &lt;/div&gt;
              &lt;/td&gt;
            &lt;/tr&gt;
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;
      &lt;!-- 購物車列表 --&gt;
      &lt;div class="col-md-5"&gt;
        &lt;div class="sticky-top"&gt;
          &lt;table class="table align-middle"&gt;
            &lt;thead&gt;
              &lt;tr&gt;
                &lt;th&gt;&lt;/th&gt;
                &lt;th&gt;品名&lt;/th&gt;
                &lt;th style="width: 110px"&gt;數量&lt;/th&gt;
                &lt;th&gt;單價&lt;/th&gt;
              &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
            &lt;template v-if="cart.carts"&gt;
              &lt;tr v-for="item in cart.carts" :key="item.id"&gt;
                &lt;td&gt;
                  &lt;button type="button" class="btn btn-outline-danger btn-sm"
                  :disabled="status.loadingItem === item.id"
                  @click="removeCartItem(item.id)"&gt;
                    &lt;i class="bi bi-x"&gt;&lt;/i&gt;
                  &lt;/button&gt;
                &lt;/td&gt;
                &lt;td&gt;
                  {{ item.product.title }}
                  &lt;div class="text-success" v-if="item.coupon"&gt;
                    已套用優惠券
                  &lt;/div&gt;
                &lt;/td&gt;
                &lt;td&gt;
                  &lt;div class="input-group input-group-sm"&gt;
                    &lt;input type="number" class="form-control"
                          v-model.number="item.qty"&gt;
                    &lt;div class="input-group-text"&gt;/ {{ item.product.unit }}&lt;/div&gt;
                  &lt;/div&gt;
                &lt;/td&gt;
                &lt;td class="text-end"&gt;
                  &lt;small v-if="cart.final_total !== cart.total" class="text-success"&gt;折扣價：&lt;/small&gt;
                  {{ $filters.currency(item.final_total) }}
                &lt;/td&gt;
              &lt;/tr&gt;
            &lt;/template&gt;
            &lt;/tbody&gt;
            &lt;tfoot&gt;
            &lt;tr&gt;
              &lt;td colspan="3" class="text-end"&gt;總計&lt;/td&gt;
              &lt;td class="text-end"&gt;{{ $filters.currency(cart.total) }}&lt;/td&gt;
            &lt;/tr&gt;
            &lt;tr v-if="cart.final_total !== cart.total"&gt;
              &lt;td colspan="3" class="text-end text-success"&gt;折扣價&lt;/td&gt;
              &lt;td class="text-end text-success"&gt;{{ $filters.currency(cart.final_total) }}&lt;/td&gt;
            &lt;/tr&gt;
            &lt;/tfoot&gt;
          &lt;/table&gt;
          &lt;div class="input-group mb-3 input-group-sm"&gt;
            &lt;input type="text" class="form-control" v-model="coupon_code" placeholder="請輸入優惠碼"&gt;
            &lt;div class="input-group-append"&gt;
              &lt;button class="btn btn-outline-secondary" type="button" @click="addCouponCode"&gt;
                套用優惠碼
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      products: &#91;],
      product: {},
      // 讀取狀態
      status: {
        loadingItem: '', // 對應品項 id
      },
      cart: {},
      coupon_code: '',
    };
  },
  methods: {
    // 取得商品列表
    getProducts() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/products/all`;
      this.isLoading = true;
      this.$http.get(url).then((response) =&gt; {
        this.products = response.data.products;
        console.log('products:', response);
        this.isLoading = false;
      });
    },
    // 進入單一商品頁面
    getProduct(id) {
      this.$router.push(`/user/product/${id}`);
    },
    // 加入購物車
    addCart(id) {
      // 測試 id 有沒有正確取出
      // console.log(id);
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/cart`;
      this.status.loadingItem = id;
      const cart = {
        product_id: id,
        qty: 1,
      };
      this.$http.post(url, { data: cart })
        .then((res) =&gt; {
          this.status.loadingItem = '';
          console.log(res);
        });
    },
    // 取得購物車列表
    getCart() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/cart`;
      this.isLoading = true;
      this.$http.get(url).then((response) =&gt; {
        console.log(response);
        this.cart = response.data.data;
        this.isLoading = false;
      });
    },
  },
  created() {
    this.getProducts();
    this.getCart();
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/main.js
import { createApp } from 'vue';
import axios from 'axios';
import VueAxios from 'vue-axios';
import Loading from 'vue3-loading-overlay';
import 'vue3-loading-overlay/dist/vue3-loading-overlay.css';
import 'bootstrap-icons/font/bootstrap-icons.css';

import App from './App.vue';
import router from './router';
import { currency, date } from './methods/filters';
import $httpMessageState from './methods/pushMessageState';

const app = createApp(App);
app.config.globalProperties.$filters = {
  currency,
  date,
};
// 此函式的用途是整合 AJAX 的錯誤事件，統一整理發送給予 Toast 處理
app.config.globalProperties.$httpMessageState = $httpMessageState;

app.use(VueAxios, axios);
app.use(router);
app.component('Loading', Loading);
app.mount('#app');
</code></pre>



<h3 class="wp-block-heading">用戶端更新購物車數量品項</h3>



<ol class="wp-block-list">
<li>看資料的部分<br>使用 Vue.js devtools 查看元件裡面的資料，購物車品項<br>&lt;RouterView&gt; &gt; &lt;UserCart&gt;</li>



<li>在 UserCart.vue 檔案<br>可以看到 v-model.number=”item.qty”，item 指的是 product 品項，目前品項可以透過 input 來調整數量多寡，Vue.js devtools 的 qty 也會跟著做調整，目前 input 已經跟資料綁上</li>



<li>現在有個問題是調整數量的時候，是可以變為 0 或者負值的，要避免變成負值，送出才不會產生問題，在 input 必須加上最小值 min = 1，避免用戶將值調成低於 0 的狀態</li>



<li>看購物車的數量，當調整數量的時候，qty 數值是會跟著做改變，但是價格是不會跟著做改變，主要是因為價格是由後端所計算出，因此必須調整後端的購物車品項的數量，才會回傳新的價格</li>



<li>查看 API 文件更新購物車的部分，更新購物車 api 跟加入購物車很像，在更新購物車會加入購物車的 id，單一品項的 id，所傳遞的參數內容和加入購物車是一樣的</li>



<li>在 Vue.js devtools 購物車裡面有 id、產品的 id，這個 id 就是在購物車裡面單一品項的 id，另一個是產品的 id，因此必須把這兩個資訊傳到後端，才能計算新的金額</li>



<li>在程式碼數量的地方加上事件，使用 change 方法觸發更新購物車 @change=”updateCart()”，因為要傳送相關的資訊，購物車的品項 id 以及產品 id，在這個地方把 item 帶進來</li>



<li>在 JS 部分撰寫 updateCart() 方法<br>加上 api 路徑、api 方法，會使用 put 方法<br>組裝要傳遞的資訊，透過物件方式來進行傳遞<br>使用 console 測試內容有沒有確實更新，在更新購物車項目之後，可以再重新取得完整購物車的內容</li>



<li>避免用戶重複點擊，加上讀取的效果<br>this.status.loadingItem = item.id; 然後更新後清空<br>在 HTML 部分加上 :disabled=”item.id === status.loadingItem”</li>
</ol>



<pre class="wp-block-code"><code>// views/UserCart.vue
&lt;template&gt;
  &lt;Loading :active="isLoading"&gt;&lt;/Loading&gt;
  &lt;div class="container"&gt;
    &lt;div class="row mt-4"&gt;
      &lt;div class="col-md-7"&gt;
        &lt;table class="table align-middle"&gt;
          &lt;thead&gt;
            &lt;tr&gt;
              &lt;th&gt;圖片&lt;/th&gt;
              &lt;th&gt;商品名稱&lt;/th&gt;
              &lt;th&gt;價格&lt;/th&gt;
              &lt;th&gt;&lt;/th&gt;
            &lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            &lt;tr v-for="item in products" :key="item.id"&gt;
              &lt;td style="width: 200px"&gt;
                &lt;div style="height: 100px; background-size: cover; background-position: center"
                :style="{backgroundImage: `url(${item.imageUrl})`}"&gt;&lt;/div&gt;
              &lt;/td&gt;
              &lt;td&gt;&lt;a href="#" class="text-dark"&gt;{{ item.title }}&lt;/a&gt;&lt;/td&gt;
              &lt;td&gt;
                &lt;div class="h5" v-if="!item.price"&gt;{{ item.origin_price }} 元&lt;/div&gt;
                &lt;del class="h6" v-if="item.price"&gt;原價 {{ item.origin_price }} 元&lt;/del&gt;
                &lt;div class="h5" v-if="item.price"&gt;現在只要 {{ item.price }} 元&lt;/div&gt;
              &lt;/td&gt;
              &lt;td&gt;
                &lt;div class="btn-group btn-group-sm"&gt;
                  &lt;button type="button" class="btn btn-outline-secondary"
                  @click="getProduct(item.id)"&gt;
                    查看更多
                  &lt;/button&gt;
                  &lt;button type="button" class="btn btn-outline-danger"
                  @click="addCart(item.id)"
                  :disabled="this.status.loadingItem === item.id"&gt;
                    &lt;div v-if="this.status.loadingItem === item.id"
                    class="spinner-grow text-danger spinner-grow-sm" role="status"&gt;
                      &lt;span class="visually-hidden"&gt;Loading...&lt;/span&gt;
                    &lt;/div&gt;
                    加到購物車
                  &lt;/button&gt;
                &lt;/div&gt;
              &lt;/td&gt;
            &lt;/tr&gt;
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;
      &lt;!-- 購物車列表 --&gt;
      &lt;div class="col-md-5"&gt;
        &lt;div class="sticky-top"&gt;
          &lt;table class="table align-middle"&gt;
            &lt;thead&gt;
              &lt;tr&gt;
                &lt;th&gt;&lt;/th&gt;
                &lt;th&gt;品名&lt;/th&gt;
                &lt;th style="width: 110px"&gt;數量&lt;/th&gt;
                &lt;th&gt;單價&lt;/th&gt;
              &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
            &lt;template v-if="cart.carts"&gt;
              &lt;tr v-for="item in cart.carts" :key="item.id"&gt;
                &lt;td&gt;
                  &lt;button type="button" class="btn btn-outline-danger btn-sm"
                  :disabled="status.loadingItem === item.id"
                  @click="removeCartItem(item.id)"&gt;
                    &lt;i class="bi bi-x"&gt;&lt;/i&gt;
                  &lt;/button&gt;
                &lt;/td&gt;
                &lt;td&gt;
                  {{ item.product.title }}
                  &lt;div class="text-success" v-if="item.coupon"&gt;
                    已套用優惠券
                  &lt;/div&gt;
                &lt;/td&gt;
                &lt;td&gt;
                  &lt;div class="input-group input-group-sm"&gt;
                    &lt;input type="number" class="form-control"
                    min="1"
                    :disabled="item.id === status.loadingItem"
                    @change="updateCart(item)"
                    v-model.number="item.qty"&gt;
                    &lt;div class="input-group-text"&gt;/ {{ item.product.unit }}&lt;/div&gt;
                  &lt;/div&gt;
                &lt;/td&gt;
                &lt;td class="text-end"&gt;
                  &lt;small v-if="cart.final_total !== cart.total" class="text-success"&gt;折扣價：&lt;/small&gt;
                  {{ $filters.currency(item.final_total) }}
                &lt;/td&gt;
              &lt;/tr&gt;
            &lt;/template&gt;
            &lt;/tbody&gt;
            &lt;tfoot&gt;
            &lt;tr&gt;
              &lt;td colspan="3" class="text-end"&gt;總計&lt;/td&gt;
              &lt;td class="text-end"&gt;{{ $filters.currency(cart.total) }}&lt;/td&gt;
            &lt;/tr&gt;
            &lt;tr v-if="cart.final_total !== cart.total"&gt;
              &lt;td colspan="3" class="text-end text-success"&gt;折扣價&lt;/td&gt;
              &lt;td class="text-end text-success"&gt;{{ $filters.currency(cart.final_total) }}&lt;/td&gt;
            &lt;/tr&gt;
            &lt;/tfoot&gt;
          &lt;/table&gt;
          &lt;div class="input-group mb-3 input-group-sm"&gt;
            &lt;input type="text" class="form-control" v-model="coupon_code" placeholder="請輸入優惠碼"&gt;
            &lt;div class="input-group-append"&gt;
              &lt;button class="btn btn-outline-secondary" type="button" @click="addCouponCode"&gt;
                套用優惠碼
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      products: &#91;],
      product: {},
      // 讀取狀態
      status: {
        loadingItem: '', // 對應品項 id
      },
      cart: {},
      coupon_code: '',
    };
  },
  methods: {
    // 取得商品列表
    getProducts() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/products/all`;
      this.isLoading = true;
      this.$http.get(url).then((response) =&gt; {
        this.products = response.data.products;
        console.log('products:', response);
        this.isLoading = false;
      });
    },
    // 進入單一商品頁面
    getProduct(id) {
      this.$router.push(`/user/product/${id}`);
    },
    // 加入購物車
    addCart(id) {
      // 測試 id 有沒有正確取出
      // console.log(id);
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/cart`;
      this.status.loadingItem = id;
      const cart = {
        product_id: id,
        qty: 1,
      };
      this.$http.post(url, { data: cart })
        .then((res) =&gt; {
          this.status.loadingItem = '';
          console.log(res);
        });
    },
    // 取得購物車列表
    getCart() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/cart`;
      this.isLoading = true;
      this.$http.get(url).then((response) =&gt; {
        console.log(response);
        this.cart = response.data.data;
        this.isLoading = false;
      });
    },
    // 更新購物車
    updateCart(item) {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/cart/${item.id}`;
      this.isLoading = true;
      // 讀取效果
      this.status.loadingItem = item.id;
      // 傳遞的資訊
      const cart = {
        product_id: item.product_id,
        qty: item.qty,
      };
      this.$http.put(url, { data: cart }).then((res) =&gt; {
        console.log(res);
        this.status.loadingItem = '';
        this.getCart();
      });
    },
  },
  created() {
    this.getProducts();
    this.getCart();
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">用戶端套用優惠券</h3>



<ol class="wp-block-list">
<li>要使用優惠券的功能務必先把優惠券的後台完成，才可以套用優惠券的功能<br>至少先新增一個優惠券<br>優惠券名稱、到期日、是否啟用、折扣百分比會直接套用在購物車內的所有的品項</li>



<li>在購物車列表 console 看一下關於購物車的內容<br>購物車品項有兩個價格 total、final_total，實際上套用優惠券會把 final_total 金額重新計算，再加總到最終的總金額，品項最終會套用到 final_total 裡面重新運算</li>



<li>以範例講解套用優惠券試試，看一下重新取得的列表內容，cart 所更新的購物車內容 final_total 的金額成功打折，另外把優惠券相關內容加入進來，品項最終會用打折後的價格方式進行計算</li>



<li>試著完成優惠券功能<br>套用優惠券按鈕加上 addCouponCode 方法，在程式碼把 addCouponCode() 方法加入<br>查看 API 文件套用優惠券 api，會發送 data 內容，data 裡面有一個 code 的屬性就是優惠券的名稱，優惠券名稱放在這個地方送出就可以了<br>api 路徑、建立資料結構、this.$http.post 方法把 url 以及 coupon 內容帶進來，包在 data 的屬性下並且把上面的 coupon 帶進來，加上 then 回傳結果，然後重新取得購物車內容，把金額更新</li>



<li>加入購物車，重新整理後購物車列表就會新增一個品項<br>套用優惠券，查看 console、折扣相關訊息呈現在畫面</li>



<li>製作 Coupon 也可以把讀取的效果還有相關的回應加上</li>



<li>試著製作刪除的功能</li>
</ol>



<pre class="wp-block-code"><code>// views/UserCart.vue
&lt;template&gt;
  &lt;Loading :active="isLoading"&gt;&lt;/Loading&gt;
  &lt;div class="container"&gt;
    &lt;div class="row mt-4"&gt;
      &lt;div class="col-md-7"&gt;
        &lt;table class="table align-middle"&gt;
          &lt;thead&gt;
            &lt;tr&gt;
              &lt;th&gt;圖片&lt;/th&gt;
              &lt;th&gt;商品名稱&lt;/th&gt;
              &lt;th&gt;價格&lt;/th&gt;
              &lt;th&gt;&lt;/th&gt;
            &lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            &lt;tr v-for="item in products" :key="item.id"&gt;
              &lt;td style="width: 200px"&gt;
                &lt;div style="height: 100px; background-size: cover; background-position: center"
                :style="{backgroundImage: `url(${item.imageUrl})`}"&gt;&lt;/div&gt;
              &lt;/td&gt;
              &lt;td&gt;&lt;a href="#" class="text-dark"&gt;{{ item.title }}&lt;/a&gt;&lt;/td&gt;
              &lt;td&gt;
                &lt;div class="h5" v-if="!item.price"&gt;{{ item.origin_price }} 元&lt;/div&gt;
                &lt;del class="h6" v-if="item.price"&gt;原價 {{ item.origin_price }} 元&lt;/del&gt;
                &lt;div class="h5" v-if="item.price"&gt;現在只要 {{ item.price }} 元&lt;/div&gt;
              &lt;/td&gt;
              &lt;td&gt;
                &lt;div class="btn-group btn-group-sm"&gt;
                  &lt;button type="button" class="btn btn-outline-secondary"
                  @click="getProduct(item.id)"&gt;
                    查看更多
                  &lt;/button&gt;
                  &lt;button type="button" class="btn btn-outline-danger"
                  @click="addCart(item.id)"
                  :disabled="this.status.loadingItem === item.id"&gt;
                    &lt;div v-if="this.status.loadingItem === item.id"
                    class="spinner-grow text-danger spinner-grow-sm" role="status"&gt;
                      &lt;span class="visually-hidden"&gt;Loading...&lt;/span&gt;
                    &lt;/div&gt;
                    加到購物車
                  &lt;/button&gt;
                &lt;/div&gt;
              &lt;/td&gt;
            &lt;/tr&gt;
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;
      &lt;!-- 購物車列表 --&gt;
      &lt;div class="col-md-5"&gt;
        &lt;div class="sticky-top"&gt;
          &lt;table class="table align-middle"&gt;
            &lt;thead&gt;
              &lt;tr&gt;
                &lt;th&gt;&lt;/th&gt;
                &lt;th&gt;品名&lt;/th&gt;
                &lt;th style="width: 110px"&gt;數量&lt;/th&gt;
                &lt;th&gt;單價&lt;/th&gt;
              &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
            &lt;template v-if="cart.carts"&gt;
              &lt;tr v-for="item in cart.carts" :key="item.id"&gt;
                &lt;td&gt;
                  &lt;button type="button" class="btn btn-outline-danger btn-sm"
                  :disabled="status.loadingItem === item.id"
                  @click="removeCartItem(item.id)"&gt;
                    &lt;i class="bi bi-x"&gt;&lt;/i&gt;
                  &lt;/button&gt;
                &lt;/td&gt;
                &lt;td&gt;
                  {{ item.product.title }}
                  &lt;div class="text-success" v-if="item.coupon"&gt;
                    已套用優惠券
                  &lt;/div&gt;
                &lt;/td&gt;
                &lt;td&gt;
                  &lt;div class="input-group input-group-sm"&gt;
                    &lt;input type="number" class="form-control"
                    min="1"
                    :disabled="item.id === status.loadingItem"
                    @change="updateCart(item)"
                    v-model.number="item.qty"&gt;
                    &lt;div class="input-group-text"&gt;/ {{ item.product.unit }}&lt;/div&gt;
                  &lt;/div&gt;
                &lt;/td&gt;
                &lt;td class="text-end"&gt;
                  &lt;small v-if="cart.final_total !== cart.total" class="text-success"&gt;折扣價：&lt;/small&gt;
                  {{ $filters.currency(item.final_total) }}
                &lt;/td&gt;
              &lt;/tr&gt;
            &lt;/template&gt;
            &lt;/tbody&gt;
            &lt;tfoot&gt;
            &lt;tr&gt;
              &lt;td colspan="3" class="text-end"&gt;總計&lt;/td&gt;
              &lt;td class="text-end"&gt;{{ $filters.currency(cart.total) }}&lt;/td&gt;
            &lt;/tr&gt;
            &lt;tr v-if="cart.final_total !== cart.total"&gt;
              &lt;td colspan="3" class="text-end text-success"&gt;折扣價&lt;/td&gt;
              &lt;td class="text-end text-success"&gt;{{ $filters.currency(cart.final_total) }}&lt;/td&gt;
            &lt;/tr&gt;
            &lt;/tfoot&gt;
          &lt;/table&gt;
          &lt;div class="input-group mb-3 input-group-sm"&gt;
            &lt;input type="text" class="form-control" v-model="coupon_code" placeholder="請輸入優惠碼"&gt;
            &lt;div class="input-group-append"&gt;
              &lt;button class="btn btn-outline-secondary" type="button" @click="addCouponCode"&gt;
                套用優惠碼
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      products: &#91;],
      product: {},
      // 讀取狀態
      status: {
        loadingItem: '', // 對應品項 id
      },
      cart: {},
      coupon_code: '',
    };
  },
  methods: {
    // 取得商品列表
    getProducts() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/products/all`;
      this.isLoading = true;
      this.$http.get(url).then((response) =&gt; {
        this.products = response.data.products;
        console.log('products:', response);
        this.isLoading = false;
      });
    },
    // 進入單一商品頁面
    getProduct(id) {
      this.$router.push(`/user/product/${id}`);
    },
    // 加入購物車
    addCart(id) {
      // 測試 id 有沒有正確取出
      // console.log(id);
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/cart`;
      this.status.loadingItem = id;
      const cart = {
        product_id: id,
        qty: 1,
      };
      this.$http.post(url, { data: cart })
        .then((res) =&gt; {
          this.status.loadingItem = '';
          console.log(res);
          // 重新取得購物車列表
          this.getCart();
        });
    },
    // 取得購物車列表
    getCart() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/cart`;
      this.isLoading = true;
      this.$http.get(url).then((response) =&gt; {
        console.log(response);
        this.cart = response.data.data;
        this.isLoading = false;
      });
    },
    // 刪除購物車品項
    removeCartItem(id) {
      this.status.loadingItem = id;
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/cart/${id}`;
      this.isLoading = true;
      this.$http.delete(url).then((response) =&gt; {
        this.$httpMessageState(response, '移除購物車品項');
        this.status.loadingItem = '';
        this.getCart();
        this.isLoading = false;
      });
    },
    // 更新購物車
    updateCart(item) {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/cart/${item.id}`;
      this.isLoading = true;
      // 讀取效果
      this.status.loadingItem = item.id;
      // 傳遞的資訊
      const cart = {
        product_id: item.product_id,
        qty: item.qty,
      };
      this.$http.put(url, { data: cart }).then((res) =&gt; {
        console.log(res);
        this.status.loadingItem = '';
        this.getCart();
      });
    },
    // 套用優惠碼
    addCouponCode() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/coupon`;
      // 建立資料結構
      const coupon = {
        code: this.coupon_code,
      };
      // 讀取效果
      this.isLoading = true;
      this.$http.post(url, { data: coupon })
        .then((res) =&gt; {
          console.log(res);
          this.getCart();
          this.isLoading = false;
        });
    },
  },
  created() {
    this.getProducts();
    this.getCart();
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">用戶端建立訂單</h3>



<ol class="wp-block-list">
<li>購物車已經有部分的品項</li>



<li>製作結帳的部分</li>



<li>建立結帳表單結構</li>



<li>安裝、套用 VeeValidate<br>npm i vee-validate @vee-validate/rules @vee-validate/i18n –save<br>注意: import * as AllRules from ‘@vee-validate/rules’;</li>



<li>查看購物車 api 的部分，使用結帳頁面的 api<br>結帳頁面 [參數] – data、message，建立表單的時候結構要注意</li>



<li>在 UserCart.vue 檔案建立結構是使用 form 物件包著 user 以及 message 區塊</li>



<li>建立訂單在 &lt;Form&gt; 標籤加上 @submit 行為觸發強制驗證，使用 createOrder 方法<br>在程式碼 methods 方法加上 createOrder 方法，把 createOrder 加到 message 裡面，把 @submit 加在 &lt;Form&gt; 標籤上的好處是當送出表單的時候會強制先進行驗證，除非內容都符合資格，否則表但不會送出<br>在 createOrder 可以先把 api 的網址建立起來<br>建立表單的結構<br>使用 this.$http.post 方法，帶上 url 路徑、使用物件把 data 屬性帶上 order 的方式把表單內容送出<br>使用 console 查看回應有沒有正確</li>



<li>送出訂單要先確定購物車裡面是有品項，再把下面的內容填寫，送出表單查看回應的結果</li>
</ol>



<pre class="wp-block-code"><code>// src/main.js
import { createApp } from 'vue';
import axios from 'axios';
import VueAxios from 'vue-axios';
import Loading from 'vue3-loading-overlay';
import 'vue3-loading-overlay/dist/vue3-loading-overlay.css';
import 'bootstrap-icons/font/bootstrap-icons.css';
import {
  Form, Field, ErrorMessage, defineRule, configure,
} from 'vee-validate';
// import AllRules from '@vee-validate/rules'; // 會產生錯誤
import * as AllRules from '@vee-validate/rules';
import { localize, setLocale } from '@vee-validate/i18n';
import zhTW from '@vee-validate/i18n/dist/locale/zh_TW.json';

import App from './App.vue';
import router from './router';
import { currency, date } from './methods/filters';
import $httpMessageState from './methods/pushMessageState';

const app = createApp(App);
app.config.globalProperties.$filters = {
  currency,
  date,
};

Object.keys(AllRules).forEach((rule) =&gt; {
  defineRule(rule, AllRules&#91;rule]);
});

configure({
  generateMessage: localize({ zh_TW: zhTW }), // 載入繁體中文語系
  validateOnInput: true, // 當輸入任何內容直接進行驗證
});
// 設定預設語系
setLocale('zh_TW');

// 此函式的用途是整合 AJAX 的錯誤事件，統一整理發送給予 Toast 處理
app.config.globalProperties.$httpMessageState = $httpMessageState;

app.use(VueAxios, axios);
app.use(router);
app.component('Loading', Loading);
app.component('Form', Form);
app.component('Field', Field);
app.component('ErrorMessage', ErrorMessage);
app.mount('#app');
</code></pre>



<pre class="wp-block-code"><code>// views/UserCart.vue
&lt;template&gt;
  &lt;Loading :active="isLoading"&gt;&lt;/Loading&gt;
  &lt;div class="container"&gt;
    &lt;div class="row mt-4"&gt;
      &lt;div class="col-md-7"&gt;
        &lt;table class="table align-middle"&gt;
          &lt;thead&gt;
            &lt;tr&gt;
              &lt;th&gt;圖片&lt;/th&gt;
              &lt;th&gt;商品名稱&lt;/th&gt;
              &lt;th&gt;價格&lt;/th&gt;
              &lt;th&gt;&lt;/th&gt;
            &lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            &lt;tr v-for="item in products" :key="item.id"&gt;
              &lt;td style="width: 200px"&gt;
                &lt;div style="height: 100px; background-size: cover; background-position: center"
                :style="{backgroundImage: `url(${item.imageUrl})`}"&gt;&lt;/div&gt;
              &lt;/td&gt;
              &lt;td&gt;&lt;a href="#" class="text-dark"&gt;{{ item.title }}&lt;/a&gt;&lt;/td&gt;
              &lt;td&gt;
                &lt;div class="h5" v-if="!item.price"&gt;{{ item.origin_price }} 元&lt;/div&gt;
                &lt;del class="h6" v-if="item.price"&gt;原價 {{ item.origin_price }} 元&lt;/del&gt;
                &lt;div class="h5" v-if="item.price"&gt;現在只要 {{ item.price }} 元&lt;/div&gt;
              &lt;/td&gt;
              &lt;td&gt;
                &lt;div class="btn-group btn-group-sm"&gt;
                  &lt;button type="button" class="btn btn-outline-secondary"
                  @click="getProduct(item.id)"&gt;
                    查看更多
                  &lt;/button&gt;
                  &lt;button type="button" class="btn btn-outline-danger"
                  @click="addCart(item.id)"
                  :disabled="this.status.loadingItem === item.id"&gt;
                    &lt;div v-if="this.status.loadingItem === item.id"
                    class="spinner-grow text-danger spinner-grow-sm" role="status"&gt;
                      &lt;span class="visually-hidden"&gt;Loading...&lt;/span&gt;
                    &lt;/div&gt;
                    加到購物車
                  &lt;/button&gt;
                &lt;/div&gt;
              &lt;/td&gt;
            &lt;/tr&gt;
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;
      &lt;!-- 購物車列表 --&gt;
      &lt;div class="col-md-5"&gt;
        &lt;div class="sticky-top"&gt;
          &lt;table class="table align-middle"&gt;
            &lt;thead&gt;
              &lt;tr&gt;
                &lt;th&gt;&lt;/th&gt;
                &lt;th&gt;品名&lt;/th&gt;
                &lt;th style="width: 110px"&gt;數量&lt;/th&gt;
                &lt;th&gt;單價&lt;/th&gt;
              &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
            &lt;template v-if="cart.carts"&gt;
              &lt;tr v-for="item in cart.carts" :key="item.id"&gt;
                &lt;td&gt;
                  &lt;button type="button" class="btn btn-outline-danger btn-sm"
                  :disabled="status.loadingItem === item.id"
                  @click="removeCartItem(item.id)"&gt;
                    &lt;i class="bi bi-x"&gt;&lt;/i&gt;
                  &lt;/button&gt;
                &lt;/td&gt;
                &lt;td&gt;
                  {{ item.product.title }}
                  &lt;div class="text-success" v-if="item.coupon"&gt;
                    已套用優惠券
                  &lt;/div&gt;
                &lt;/td&gt;
                &lt;td&gt;
                  &lt;div class="input-group input-group-sm"&gt;
                    &lt;input type="number" class="form-control"
                    min="1"
                    :disabled="item.id === status.loadingItem"
                    @change="updateCart(item)"
                    v-model.number="item.qty"&gt;
                    &lt;div class="input-group-text"&gt;/ {{ item.product.unit }}&lt;/div&gt;
                  &lt;/div&gt;
                &lt;/td&gt;
                &lt;td class="text-end"&gt;
                  &lt;small v-if="cart.final_total !== cart.total" class="text-success"&gt;折扣價：&lt;/small&gt;
                  {{ $filters.currency(item.final_total) }}
                &lt;/td&gt;
              &lt;/tr&gt;
            &lt;/template&gt;
            &lt;/tbody&gt;
            &lt;tfoot&gt;
            &lt;tr&gt;
              &lt;td colspan="3" class="text-end"&gt;總計&lt;/td&gt;
              &lt;td class="text-end"&gt;{{ $filters.currency(cart.total) }}&lt;/td&gt;
            &lt;/tr&gt;
            &lt;tr v-if="cart.final_total !== cart.total"&gt;
              &lt;td colspan="3" class="text-end text-success"&gt;折扣價&lt;/td&gt;
              &lt;td class="text-end text-success"&gt;{{ $filters.currency(cart.final_total) }}&lt;/td&gt;
            &lt;/tr&gt;
            &lt;/tfoot&gt;
          &lt;/table&gt;
          &lt;div class="input-group mb-3 input-group-sm"&gt;
            &lt;input type="text" class="form-control" v-model="coupon_code" placeholder="請輸入優惠碼"&gt;
            &lt;div class="input-group-append"&gt;
              &lt;button class="btn btn-outline-secondary" type="button" @click="addCouponCode"&gt;
                套用優惠碼
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;!-- 用戶端訂單 --&gt;
    &lt;div class="my-5 row justify-content-center"&gt;
      &lt;Form class="col-md-6" v-slot="{ errors }"
            @submit="createOrder"&gt;
        &lt;div class="mb-3"&gt;
          &lt;label for="email" class="form-label"&gt;Email&lt;/label&gt;
          &lt;Field id="email" name="email" type="email" class="form-control"
          :class="{ 'is-invalid': errors&#91;'email'] }"
          placeholder="請輸入 Email" rules="email|required"
          v-model="form.user.email"&gt;&lt;/Field&gt;
          &lt;ErrorMessage name="email" class="invalid-feedback"&gt;&lt;/ErrorMessage&gt;
        &lt;/div&gt;

        &lt;div class="mb-3"&gt;
          &lt;label for="name" class="form-label"&gt;收件人姓名&lt;/label&gt;
          &lt;Field id="name" name="姓名" type="text" class="form-control"
          :class="{ 'is-invalid': errors&#91;'姓名'] }"
          placeholder="請輸入姓名" rules="required"
          v-model="form.user.name"&gt;&lt;/Field&gt;
          &lt;ErrorMessage name="姓名" class="invalid-feedback"&gt;&lt;/ErrorMessage&gt;
        &lt;/div&gt;

        &lt;div class="mb-3"&gt;
          &lt;label for="tel" class="form-label"&gt;收件人電話&lt;/label&gt;
          &lt;Field id="tel" name="電話" type="tel" class="form-control"
          :class="{ 'is-invalid': errors&#91;'電話'] }"
          placeholder="請輸入電話" rules="required"
          v-model="form.user.tel"&gt;&lt;/Field&gt;
          &lt;ErrorMessage name="電話" class="invalid-feedback"&gt;&lt;/ErrorMessage&gt;
        &lt;/div&gt;

        &lt;div class="mb-3"&gt;
          &lt;label for="address" class="form-label"&gt;收件人地址&lt;/label&gt;
          &lt;Field id="address" name="地址" type="text" class="form-control"
          :class="{ 'is-invalid': errors&#91;'地址'] }"
          placeholder="請輸入地址" rules="required"
          v-model="form.user.address"&gt;&lt;/Field&gt;
          &lt;ErrorMessage name="地址" class="invalid-feedback"&gt;&lt;/ErrorMessage&gt;
        &lt;/div&gt;

        &lt;div class="mb-3"&gt;
          &lt;label for="message" class="form-label"&gt;留言&lt;/label&gt;
          &lt;textarea name="" id="message" class="form-control" cols="30" rows="10"
                    v-model="form.message"&gt;&lt;/textarea&gt;
        &lt;/div&gt;
        &lt;div class="text-end"&gt;
          &lt;button class="btn btn-danger"&gt;送出訂單&lt;/button&gt;
        &lt;/div&gt;
      &lt;/Form&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      products: &#91;],
      product: {},
      // 讀取狀態
      status: {
        loadingItem: '', // 對應品項 id
      },
      cart: {},
      coupon_code: '',
      // 用戶端訂單
      form: {
        user: {
          name: '',
          email: '',
          tel: '',
          address: '',
        },
        message: '',
      },
    };
  },
  methods: {
    // 取得商品列表
    getProducts() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/products/all`;
      this.isLoading = true;
      this.$http.get(url).then((response) =&gt; {
        this.products = response.data.products;
        console.log('products:', response);
        this.isLoading = false;
      });
    },
    // 進入單一商品頁面
    getProduct(id) {
      this.$router.push(`/user/product/${id}`);
    },
    // 加入購物車
    addCart(id) {
      // 測試 id 有沒有正確取出
      // console.log(id);
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/cart`;
      this.status.loadingItem = id;
      const cart = {
        product_id: id,
        qty: 1,
      };
      this.$http.post(url, { data: cart })
        .then((res) =&gt; {
          this.status.loadingItem = '';
          console.log(res);
          // 重新取得購物車列表
          this.getCart();
        });
    },
    // 取得購物車列表
    getCart() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/cart`;
      this.isLoading = true;
      this.$http.get(url).then((response) =&gt; {
        console.log(response);
        this.cart = response.data.data;
        this.isLoading = false;
      });
    },
    // 刪除購物車品項
    removeCartItem(id) {
      this.status.loadingItem = id;
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/cart/${id}`;
      this.isLoading = true;
      this.$http.delete(url).then((response) =&gt; {
        this.$httpMessageState(response, '移除購物車品項');
        this.status.loadingItem = '';
        this.getCart();
        this.isLoading = false;
      });
    },
    // 更新購物車
    updateCart(item) {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/cart/${item.id}`;
      this.isLoading = true;
      // 讀取效果
      this.status.loadingItem = item.id;
      // 傳遞的資訊
      const cart = {
        product_id: item.product_id,
        qty: item.qty,
      };
      this.$http.put(url, { data: cart }).then((res) =&gt; {
        console.log(res);
        this.status.loadingItem = '';
        this.getCart();
      });
    },
    // 套用優惠碼
    addCouponCode() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/coupon`;
      // 建立資料結構
      const coupon = {
        code: this.coupon_code,
      };
      // 讀取效果
      this.isLoading = true;
      this.$http.post(url, { data: coupon })
        .then((res) =&gt; {
          console.log(res);
          this.getCart();
          this.isLoading = false;
        });
    },
    // 建立訂單
    createOrder() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/order`;
      const order = this.form;
      this.$http.post(url, { data: order })
        .then((res) =&gt; {
          console.log(res);
        });
    },
  },
  created() {
    this.getProducts();
    this.getCart();
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">用戶端結帳流程</h3>



<ol class="wp-block-list">
<li>建立新的一筆訂單，並填好用戶表端資料，預期在送出訂單之後會產一個 orderId，複製 orderId 的值等下使用，送出訂單之後預期會轉只到另外一個頁面，checkout 路由下的頁面，讓用戶確認產品還有表單內容是否正確，再進行結帳的流程，現在的流程是必須先把 checkout 加上再讓用戶進行結帳</li>



<li>新增 UserCheckout.vue 檔案撰寫程式碼<br>完成 checkout 頁面，上方是產品的內容，在購物車裡面的品項，下方是用戶自己所填的細節，會透過 orderId 把資訊取回來</li>



<li>著重在 JS 的部分</li>



<li>在 index.js 檔案加上子路由 checkout/:orderId，orderId 作用是取回原本同一筆訂單使用的</li>



<li>直接使用網址路徑進入用戶端結帳頁面<br>查看 API 文件取得某一筆訂單，透過 orderId 取得相同一筆訂單內容<br>必須先把路由上面的 id 取出來，在 data() 資料定義 orderId，在 created() 生命週期撰寫 this.orderId = this.$route.params.orderId 把 orderId 取出<br>使用 console 查看，orderId 是從網址列所取得<br>加上 this.getOrder() 方法，會觸發 methods 裡面的 getOrder()，會透過 getOrder 取得需要的相關內容</li>



<li>在 getOrder() 方法撰寫取得某一筆訂單<br>api 路徑取得特定訂單的內容、使用 this.$http.get 方法取得這筆訂單內容<br>使用 console 是否有正確取得<br>將相關的內容呈現在畫面上</li>



<li>製作確認付款去的行為<br>先看 order 包含什麼資訊，新增品項的時間、id、總金額、is_paid 屬性，is_paid 是用戶是否已經付款，is_paid只能由後端進行調整<br>用戶觸發結帳付款 api，觸發之後就會把 is_paid 的 false 改為 true<br>結帳付款 api 主要的重點在 orderId，會透過 orderId 搭配前一個 pay api 將這筆訂單的 is_paid 改為 true</li>



<li>針對這筆訂單進行付款<br>在 &lt;form&gt; 表單加上 payOrder 方法，當用戶按下按鈕或是送出表單的時候，就會觸發 payOrder 行為<br>撰寫 payOrder() 方法<br>url 路徑、使用 this.$http.post 方法將這筆訂單進行付款，發出請求後會使用 then 接收回傳<br>將這筆訂單完成之後就不能再次使用，重整再重新取得訂單的項目，再重新觸發一次 getOrder 行為</li>



<li>按下確認付款去之後，會觸發一個行為，會出現付款已經完成，在畫面上付款狀態會改成付款完成，確認付款去按鈕就會隱藏</li>



<li>查看 console 關於 this.order 這筆訂單的資訊 is_paid 會改為 true 的狀態，並且增加 paid_date 屬性，紀錄用戶在哪一個時間點進行付款</li>
</ol>



<pre class="wp-block-code"><code>// views/UserCheckout.vue
&lt;template&gt;
  &lt;Loading :active="isLoading"&gt;&lt;/Loading&gt;
  &lt;div class="my-5 row justify-content-center"&gt;
    &lt;form class="col-md-6" @submit.prevent="payOrder"&gt;
      &lt;table class="table align-middle"&gt;
        &lt;thead&gt;
        &lt;th&gt;品名&lt;/th&gt;
        &lt;th&gt;數量&lt;/th&gt;
        &lt;th&gt;單價&lt;/th&gt;
        &lt;/thead&gt;
        &lt;tbody&gt;
        &lt;tr v-for="item in order.products" :key="item.id"&gt;
          &lt;td&gt;{{ item.product.title }}&lt;/td&gt;
          &lt;td&gt;{{ item.qty }}/{{ item.product.unit }}&lt;/td&gt;
          &lt;td class="text-end"&gt;{{ item.final_total }}&lt;/td&gt;
        &lt;/tr&gt;
        &lt;/tbody&gt;
        &lt;tfoot&gt;
        &lt;tr&gt;
          &lt;td colspan="2" class="text-end"&gt;總計&lt;/td&gt;
          &lt;td class="text-end"&gt;{{ order.total }}&lt;/td&gt;
        &lt;/tr&gt;
        &lt;/tfoot&gt;
      &lt;/table&gt;

      &lt;table class="table"&gt;
        &lt;tbody&gt;
        &lt;tr&gt;
          &lt;th width="100"&gt;Email&lt;/th&gt;
          &lt;td&gt;{{ order.user.email }}&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
          &lt;th&gt;姓名&lt;/th&gt;
          &lt;td&gt;{{ order.user.name }}&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
          &lt;th&gt;收件人電話&lt;/th&gt;
          &lt;td&gt;{{ order.user.tel }}&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
          &lt;th&gt;收件人地址&lt;/th&gt;
          &lt;td&gt;{{ order.user.address }}&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
          &lt;th&gt;付款狀態&lt;/th&gt;
          &lt;td&gt;
            &lt;span v-if="!order.is_paid"&gt;尚未付款&lt;/span&gt;
            &lt;span v-else class="text-success"&gt;付款完成&lt;/span&gt;
          &lt;/td&gt;
        &lt;/tr&gt;
        &lt;/tbody&gt;
      &lt;/table&gt;
      &lt;div class="text-end" v-if="order.is_paid === false"&gt;
        &lt;button class="btn btn-danger"&gt;確認付款去&lt;/button&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      order: {
        user: {},
      },
      orderId: '',
      isLoading: false,
    };
  },
  methods: {
    // 取得某一筆訂單
    getOrder() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/order/${this.orderId}`;
      this.$http.get(url)
        .then((res) =&gt; {
          if (res.data.success) {
            this.order = res.data.order;
            console.log(this.order);
          }
        });
    },
    // 確認付款
    payOrder() {
      const url = `${process.env.VUE_APP_API}api/${process.env.VUE_APP_PATH}/pay/${this.orderId}`;
      this.$http.post(url)
        .then((res) =&gt; {
          console.log(res);
          if (res.data.success) {
            this.getOrder();
          }
        });
    },
  },
  created() {
    this.orderId = this.$route.params.orderId;
    console.log(this.orderId);
    this.getOrder();
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/router/index.js
import { createRouter, createWebHashHistory } from 'vue-router';
import Home from '../views/Home.vue';

const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/login',
    component: () =&gt; import('../views/Login.vue'),
  },
  {
    path: '/dashboard',
    component: () =&gt; import('../views/Dashboard.vue'),
    // 巢狀路由 - 產品列表
    children: &#91;
      {
        path: 'products',
        component: () =&gt; import('../views/Products.vue'),
      },
      {
        path: 'orders',
        component: () =&gt; import('../views/Orders.vue'),
      },
      {
        path: 'coupons',
        component: () =&gt; import('../views/Coupons.vue'),
      },
    ],
  },
  {
    path: '/user',
    component: () =&gt; import('../views/Userboard.vue'),
    children: &#91;
      {
        path: 'cart',
        component: () =&gt; import('../views/UserCart.vue'),
      },
      {
        path: 'product/:productId',
        component: () =&gt; import('@/views/UserProduct.vue'),
      },
      {
        path: 'checkout/:orderId',
        component: () =&gt; import('../views/UserCheckout.vue'),
      },
    ],
  },
];

const router = createRouter({
  history: createWebHashHistory(),
  routes,
  linkActiveClass: 'active',
});

export default router;
</code></pre>



<h3 class="wp-block-heading">最終作業說明</h3>



<h4 class="wp-block-heading">最終作業繳交說明</h4>



<h5 class="wp-block-heading">一般練習</h5>



<ul class="wp-block-list">
<li>僅檢視原始碼</li>



<li>僅需完成課程基本條件</li>



<li>僅需要完成桌面版</li>
</ul>



<h5 class="wp-block-heading">求職作品</h5>



<ul class="wp-block-list">
<li>檢視畫面設計及原始碼</li>



<li>除了課程 API 需增加自己創意在內</li>



<li>行動版、桌面版皆需製作</li>



<li>已正式上線為目標</li>



<li>退件率高</li>



<li><strong>請標示作為求職作品使用</strong></li>
</ul>



<h4 class="wp-block-heading">作品製作建議</h4>



<h5 class="wp-block-heading">先決定主題</h5>



<ul class="wp-block-list">
<li>搜集素材</li>



<li>確認主題製作可行性</li>



<li>定義產品資訊</li>



<li>構思技術小巧思 (我的最愛、抽獎、地圖功能、倒數優惠…)</li>



<li>開始執行</li>
</ul>



<h4 class="wp-block-heading">素材資源</h4>



<ul class="wp-block-list">
<li><a href="https://getbootstrap.com/" target="_blank" rel="noreferrer noopener">Bootstrap</a>、<a href="https://bootstrap5.hexschool.com/" target="_blank" rel="noreferrer noopener">Bootstrap 中文版</a></li>



<li><a href="https://material.io/design" target="_blank" rel="noreferrer noopener">Material Design</a>、<a href="https://material-design.hexschool.io/" target="_blank" rel="noreferrer noopener">Material Design 中文版</a></li>



<li><a href="https://unsplash.com/" target="_blank" rel="noreferrer noopener">圖庫</a></li>



<li><a href="https://fonts.google.com/icons" target="_blank" rel="noreferrer noopener">Material Design Icon</a></li>



<li><a href="https://icons.getbootstrap.com/#icons" target="_blank" rel="noreferrer noopener">Bootstrap Icon</a></li>



<li><a href="https://fontawesome.com/" target="_blank" rel="noreferrer noopener">FontAwesome</a></li>



<li><a href="https://www.flaticon.com/" target="_blank" rel="noreferrer noopener">付費 icon</a></li>



<li><a href="https://www.google.com/get/noto/" target="_blank" rel="noreferrer noopener">字體 (無襯線、san-serif、黑體)</a></li>
</ul>



<h4 class="wp-block-heading">你的用心，會讓作品更加耀眼</h4>



<h5 class="wp-block-heading">盡可能避免的錯誤</h5>



<ul class="wp-block-list">
<li>缺少文案、作品不完整</li>



<li>沒有對齊、畫面跑版</li>



<li>使用者體驗的問題: 例如購物車為空時，避免讓用戶進入下一步</li>



<li>行動版無法運作</li>
</ul>



<h4 class="wp-block-heading">不知如何下手嗎</h4>



<h5 class="wp-block-heading">萬丈高樓平地起，規劃優於一切</h5>



<ul class="wp-block-list">
<li>先參考學長姐的作品</li>



<li>從搜集資料開始，規劃網站的文案、產品、各種小巧思</li>



<li>先製作版型，將流程一一的順好</li>



<li>將功能一一套上版型</li>



<li>最後修正，提交作業</li>
</ul>



<h3 class="wp-block-heading">最終作業提交規則文件</h3>



<h3 class="wp-block-heading">最終作業提交 – 程式勇者村</h3>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Vue3 複習08</title>
		<link>/wordpress_blog/reviewvue3_08/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Wed, 20 Mar 2024 10:02:00 +0000</pubDate>
				<category><![CDATA[六角學院]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=833</guid>

					<description><![CDATA[Vue Router 本章節以後 “發問的重要說明” 由於本章節 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">Vue Router</h2>



<h3 class="wp-block-heading">本章節以後 “發問的重要說明”</h3>



<p>由於本章節以後，問題相對會複雜很多，很難從單一片段了解問題點。</p>



<p>如：部分程式碼無法運作，實際卻是其它片段的大小寫錯誤</p>



<p>所以<strong>本章節以後，為了加速回覆同學們問題的效率，請上傳完整程式碼至 Github 或個人雲端空間</strong>（不需要包含 node_modules），好讓我可以運行完整環境來仔細檢視喔。</p>



<p>如果僅上傳部分程式碼或程式碼的截圖，還是會要求重新上傳完整程式碼再行回覆，也感謝同學們的配合</p>



<h3 class="wp-block-heading">Vue Router 簡介</h3>



<h4 class="wp-block-heading">關於路由 (MVC 概念)</h4>



<h4 class="wp-block-heading">後端路由在頁面上呈現的狀態</h4>



<p>傳統後端路由: https://www.hexschool.com/<strong>/user</strong></p>



<h4 class="wp-block-heading">前端路由在頁面上顯示的狀態</h4>



<p>前端路由: https://www.hexschool.com/<strong>#/user</strong></p>



<h4 class="wp-block-heading">專案中的概念</h4>



<p>前端路由: https://www.hexschool.com/<strong>#/user</strong><br>前端路由: https://www.hexschool.com/<strong>#/products</strong></p>



<p>路由表<br>‘/user’ &gt; User.vue<br>‘/products’ &gt; Products.vue</p>



<p>User.vue<br>Products.vue</p>



<h4 class="wp-block-heading">Vue Router 開發流程</h4>



<p>準備流程:</p>



<ol class="wp-block-list">
<li>引入 Vue Router 環境</li>



<li>定義元件</li>



<li>定義路由表</li>



<li>加入對應連結</li>
</ol>



<h3 class="wp-block-heading">Vue Router 相關資源</h3>



<ul class="wp-block-list">
<li><a href="https://next.router.vuejs.org/" target="_blank" rel="noreferrer noopener">官方文件</a></li>



<li><a href="https://next.router.vuejs.org/zh/index.html" target="_blank" rel="noreferrer noopener">中文版官方文件</a></li>
</ul>



<p>注意：Vue 3 搭配的 Router 版本在網址中會有 “next” 的字樣，如：router.vuejs.org 則是 Vue 2 版本的路由</p>



<ul class="wp-block-list">
<li><a href="https://github.com/Wcc723/Vue3-Coures-Router-Demo/commits/master" target="_blank" rel="noreferrer noopener">本章節的所有操作紀錄範例</a></li>
</ul>



<p>盡可能自己完成本章節的操作。</p>



<p>如果需要參考時，請注意 git 排序是逆向的，也可依據 Commit 名稱尋找，找到後可直接透過 hash 文字打開該章節所調整的片段</p>



<h3 class="wp-block-heading">在一般網頁中引入 Vue Router</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h2&gt;路由示範&lt;/h2&gt;
  {{ text }}
  &lt;router-link to="/a"&gt;a&lt;/router-link&gt; |
  &lt;router-link to="/b"&gt;b&lt;/router-link&gt;
  &lt;router-view&gt;&lt;/router-view&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;script src="https://unpkg.com/vue-router@4.0.5/dist/vue-router.global.js"&gt;&lt;/script&gt;

&lt;script&gt;
const componentA = {
  template: `
  &lt;div&gt;A&lt;/div&gt;
  `
};
const componentB = {
  template: `
  &lt;div&gt;B&lt;/div&gt;
  `
};

const app = Vue.createApp({
  data() {
    return {
      counter: 0,
      text: '這裡有一段文字'
    }
  }
});

// 路由設定
const router = VueRouter.createRouter({
  // 網址路徑模式：使用網址 hash 的形式
  history: VueRouter.createWebHashHistory(),
  // 匯入路由表
  routes: &#91;
    {
      // a 路徑
      path: '/a',
      // A 元件
      component: componentA,
    },
    {
      // b 路徑
      path: '/b',
      // B 元件
      component: componentB,
    },
  ]
});
app.use(router);
app.mount('#app');
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">Vue Cli 中使用 Vue Router 開發</h3>



<ol class="wp-block-list">
<li>使用 vue create vue_router_record</li>



<li>Please pick a preset: Manually select features</li>



<li>Check the features needed for your project: Choose Vue versin, Babel, Router, CSS Pre-processors, Linter</li>



<li>Choose a version of Vue.js that you want to start the project with 3.x (Preview)</li>



<li>Use history mode for router? (Requires proper server setup for index fallback in production) No</li>



<li>Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)</li>



<li>Pick a linter / formatter config: Airbnb</li>



<li>Pick additional lint features: Lint on save</li>



<li>Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files</li>



<li>Save this as a preset for future projects? N</li>



<li>運行專案 – npm run serve</li>
</ol>



<h4 class="wp-block-heading">講解</h4>



<p>Vue Cli 版本和 HTML 開發哪裡不一樣</p>



<ul class="wp-block-list">
<li>router/index.js 檔案</li>



<li>main.js 檔案</li>



<li>App.vue 檔案<br>&lt;router-view/&gt; – 主要的頁面切換<br>&lt;router-link&gt;&lt;/router-link&gt; – 用戶頁面連結點擊使用</li>



<li>views 資料夾 – 主要頁面的設定<br>components 資料夾 – 相對比較小的元件</li>
</ul>



<pre class="wp-block-code"><code>// src/router/index.js
// 匯入 vue-router 資源
import { createRouter, createWebHashHistory } from 'vue-router';
// 匯入獨立元件
import Home from '../views/Home.vue';

// 路由表
const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
];

// router 結構
const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

// 匯出
export default router;
</code></pre>



<pre class="wp-block-code"><code>// main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

createApp(App).use(router).mount('#app');
</code></pre>



<pre class="wp-block-code"><code>// src/App.vue
&lt;template&gt;
  &lt;div id="nav"&gt;
    &lt;router-link to="/"&gt;Home&lt;/router-link&gt; |
    &lt;router-link to="/about"&gt;About&lt;/router-link&gt;
  &lt;/div&gt;
  &lt;router-view/&gt;
&lt;/template&gt;

&lt;style lang="scss"&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;

  a {
    font-weight: bold;
    color: #2c3e50;

    &amp;.router-link-exact-active {
      color: #42b983;
    }
  }
}
&lt;/style&gt;
</code></pre>



<h4 class="wp-block-heading">新增元件和路由</h4>



<ol class="wp-block-list">
<li>新增一個元件加到畫面上<br>新增 views/NewPage.vue<br>原則: 先建立元件再加入路由</li>



<li>打開 router/index.js 檔案<br>在路由表新增物件內容</li>



<li>把頁面的連結加到畫面上<br>在 App.vue 檔案加入新的 &lt;router-link&gt;<br>使用另外一種寫法加入連結，改用動態屬性的方式把連結加入</li>
</ol>



<pre class="wp-block-code"><code>// views/NewPage.vue
&lt;template&gt;
  &lt;div&gt;新增頁面&lt;/div&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// router/index.js
// 匯入 vue-router 資源
import { createRouter, createWebHashHistory } from 'vue-router';
// 匯入獨立元件
import Home from '../views/Home.vue';

// 路由表
const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/newpage',
    name: '新增頁面',
    component: () =&gt; import('../views/NewPage.vue'),
  },
];

// router 結構
const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

// 匯出
export default router;
</code></pre>



<pre class="wp-block-code"><code>// src/App.vue
&lt;template&gt;
  &lt;div id="nav"&gt;
    &lt;router-link to="/"&gt;Home&lt;/router-link&gt; |
    &lt;router-link to="/about"&gt;About&lt;/router-link&gt; |
    &lt;!-- 動態屬性 --&gt;
    &lt;router-link :to="{ name: '新增頁面' }"&gt;新增頁面&lt;/router-link&gt;
  &lt;/div&gt;
  &lt;router-view/&gt;
&lt;/template&gt;

&lt;style lang="scss"&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;

  a {
    font-weight: bold;
    color: #2c3e50;

    &amp;.router-link-exact-active {
      color: #42b983;
    }
  }
}
&lt;/style&gt;
</code></pre>



<h3 class="wp-block-heading">中場: 為目前專案加入樣式</h3>



<ol class="wp-block-list">
<li>在 public/index.html 加入 Bootstrap 5 CSS CDN</li>



<li>在 App.vue 檔案調整 router-link，加入 navbar 的樣式<br>選擇相對簡單的樣式，直接複製貼到 App.vue 檔案</li>



<li>把 router-link 加進來，並調整 class 樣式</li>



<li>router-view 外層加上 .container 樣式</li>
</ol>



<pre class="wp-block-code"><code>// public/index.html
&lt;!DOCTYPE html&gt;
&lt;html lang=""&gt;
  &lt;head&gt;
    &lt;meta charset="utf-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width,initial-scale=1.0"&gt;
    &lt;link rel="icon" href="&lt;%= BASE_URL %&gt;favicon.ico"&gt;
    &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"&gt;
    &lt;title&gt;&lt;%= htmlWebpackPlugin.options.title %&gt;&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;noscript&gt;
      &lt;strong&gt;We're sorry but &lt;%= htmlWebpackPlugin.options.title %&gt; doesn't work properly without JavaScript enabled. Please enable it to continue.&lt;/strong&gt;
    &lt;/noscript&gt;
    &lt;div id="app"&gt;&lt;/div&gt;
    &lt;!-- built files will be auto injected --&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/App.vue
&lt;template&gt;
  &lt;nav class="navbar navbar-expand-lg bg-body-tertiary"&gt;
    &lt;div class="container-fluid"&gt;
      &lt;a class="navbar-brand" href="#"&gt;Navbar&lt;/a&gt;
      &lt;button class="navbar-toggler" type="button"
        data-bs-toggle="collapse" data-bs-target="#navbarNav"
        aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"&gt;
        &lt;span class="navbar-toggler-icon"&gt;&lt;/span&gt;
      &lt;/button&gt;
      &lt;div class="collapse navbar-collapse" id="navbarNav"&gt;
        &lt;ul class="navbar-nav"&gt;
          &lt;li class="nav-item"&gt;
            &lt;router-link to="/" class="nav-link active" aria-current="page"&gt;Home&lt;/router-link&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;router-link to="/about" class="nav-link"&gt;About&lt;/router-link&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;router-link :to="{ name: '新增頁面' }" class="nav-link"&gt;新增頁面&lt;/router-link&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;
  &lt;div class="container"&gt;
    &lt;router-view/&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;style lang="scss"&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;

  a {
    font-weight: bold;
    color: #2c3e50;

    &amp;.router-link-exact-active {
      color: #42b983;
    }
  }
}
&lt;/style&gt;
</code></pre>



<h3 class="wp-block-heading">巢狀路由</h3>



<ol class="wp-block-list">
<li>App.vue CSS 樣式移除</li>



<li>建立兩個元件 – ComponentA、ComponentB<br>建立的元件放在 views 資料夾</li>



<li>加入路由 – 在 router/index.js 檔案<br>要加入子項目會在 newpage 的下方加入 children 陣列形式，然後加上物件內容</li>



<li>修改 NewPage.vue 檔案</li>



<li>補上左側的選單 – Bootstrap &gt; List Group &gt; Links and buttons<br>複製程式碼貼到 NewPage.vue 選單的地方<br>把連結改成&lt;router-link&gt; 標籤、加上路徑</li>
</ol>



<pre class="wp-block-code"><code>// views/ComponentA.vue
&lt;template&gt;
  元件 A
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// views/ComponentB.vue
&lt;template&gt;
  元件 B
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// router/index.js
// 匯入 vue-router 資源
import { createRouter, createWebHashHistory } from 'vue-router';
// 匯入獨立元件
import Home from '../views/Home.vue';

// 路由表
const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/newpage',
    name: '新增頁面',
    component: () =&gt; import('../views/NewPage.vue'),
    children: &#91;
      {
        // 子項目的路徑不用加上/
        path: 'a',
        component: () =&gt; import('../views/ComponentA.vue'),
      },
      {
        path: 'b',
        component: () =&gt; import('../views/ComponentB.vue'),
      },
    ],
  },
];

// router 結構
const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

// 匯出
export default router;
</code></pre>



<pre class="wp-block-code"><code>// views/NewPage.vue
&lt;template&gt;
  &lt;div class="row"&gt;
    &lt;div class="col-4"&gt;
      &lt;div class="list-group"&gt;
        &lt;router-link to="/newpage/a" class="list-group-item list-group-item-action"&gt;
          元件 A
        &lt;/router-link&gt;
        &lt;router-link to="/newpage/b" class="list-group-item list-group-item-action"&gt;
          元件 B
        &lt;/router-link&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="col-8"&gt;
      &lt;router-view&gt;&lt;/router-view&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">一個元件插入多個視圖 – 具名視圖</h3>



<ol class="wp-block-list">
<li>新增元件 C</li>



<li>新增元件 NamedView<br>加入兩個 router-view</li>



<li>打開 router/index.js 檔案<br>路由表新增新的物件內容</li>



<li>在路由 namedView 新增 children 子路由<br>在 children 陣列加入其他子路由項目(c2a、a2b)<br>因為有兩個視圖(router-view)，所以必須載入兩個元件<br>components 有加上s，因為要載入多個元件，以物件的形式把它帶進來</li>



<li>把具名視圖(命名視圖)路徑加到左側的選單</li>
</ol>



<pre class="wp-block-code"><code>// views/ComponentC.vue
&lt;template&gt;
  元件 C
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// views/NamedView.vue
&lt;template&gt;
  &lt;h2&gt;具名視圖(命名視圖)的範例&lt;/h2&gt;
  &lt;div class="row"&gt;
    &lt;div class="col-6"&gt;
      &lt;router-view name="left"&gt;&lt;/router-view&gt;
    &lt;/div&gt;
    &lt;div class="col-6"&gt;
      &lt;router-view name="right"&gt;&lt;/router-view&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// router/index.js
// 匯入 vue-router 資源
import { createRouter, createWebHashHistory } from 'vue-router';
// 匯入獨立元件
import Home from '../views/Home.vue';

// 路由表
const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/newpage',
    name: '新增頁面',
    component: () =&gt; import('../views/NewPage.vue'),
    children: &#91;
      {
        // 子項目的路徑不用加上/
        path: 'a',
        component: () =&gt; import('../views/ComponentA.vue'),
      },
      {
        path: 'b',
        component: () =&gt; import('../views/ComponentB.vue'),
      },
      // 具名視圖(命名視圖)
      {
        path: 'namedview',
        component: () =&gt; import('../views/NamedView.vue'),
        children: &#91;
          {
            path: 'c2a',
            // 載入多個元件
            components: {
              // 命名視圖使用名稱就是物件所使用的名稱
              left: () =&gt; import('../views/ComponentC.vue'),
              right: () =&gt; import('../views/ComponentA.vue'),
            },
          },
          {
            path: 'a2b',
            components: {
              left: () =&gt; import('../views/ComponentA.vue'),
              right: () =&gt; import('../views/ComponentB.vue'),
            },
          },
        ],
      },
    ],
  },
];

// router 結構
const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

// 匯出
export default router;
</code></pre>



<pre class="wp-block-code"><code>// views/NewPage.vue
&lt;template&gt;
  &lt;div class="row"&gt;
    &lt;div class="col-4"&gt;
      &lt;div class="list-group"&gt;
        &lt;router-link to="/newpage/a" class="list-group-item list-group-item-action"&gt;
          元件 A
        &lt;/router-link&gt;
        &lt;router-link to="/newpage/b" class="list-group-item list-group-item-action"&gt;
          元件 B
        &lt;/router-link&gt;
        &lt;router-link to="/newpage/namedview/c2a" class="list-group-item list-group-item-action"&gt;
          命名路由 c2a
        &lt;/router-link&gt;
        &lt;router-link to="/newpage/namedview/a2b" class="list-group-item list-group-item-action"&gt;
          命名路由 a2b
        &lt;/router-link&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="col-8"&gt;
      &lt;router-view&gt;&lt;/router-view&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">透過參數決定路由內容 – 動態路由</h3>



<ol class="wp-block-list">
<li>安裝 axios 套件 – npm install axios</li>



<li>新增 DynamicRouter 元件 – views/DynamicRouter.vue</li>



<li>重新運行 npm run serve</li>



<li>DynamicRouter 檔案主要會用到 &lt;script&gt; 的部分<br>取得遠端資料，以 RANDOM USER 為範例<br>複製 api: https://randomuser.me/api/</li>



<li>載入 axios 套件<br>在 created 生命週期把遠端資料載進來</li>



<li>DynamicRouter 元件加到 router 路由表裡面</li>



<li>確認有沒有取得遠端資料<br>http://localhost:8080/#/newpage/dynamicrouter</li>



<li>觀察 info &gt; seed、資料用戶<br>複製 data &gt; info &gt; seed 的資料<br>修改 DynamicRouter.vue 程式碼<br>透過 id 的形式取得相同一個資料</li>



<li>id 透過網址來進行傳遞<br>http://localhost:8080/#/newpage/dynamicrouter/b9039789d7d5a209<br>修改 router/index.js 程式碼<br>path: ‘dynamicRouter/:id’,</li>



<li>動態值取出來直接使用<br>console.log(this.$route.params.id)</li>
</ol>



<pre class="wp-block-code"><code>// views/DynamicRouter.vue
&lt;template&gt;
  &lt;div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
// 載入 axios 套件
import axios from 'axios';

export default {
  created() {
    // b9039789d7d5a209
    console.log(this.$route.params.id);
    const seed = this.$route.params.id;
    axios.get(`https://randomuser.me/api/?seed=${seed}`)
      .then((res) =&gt; {
        console.log(res);
      });
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// router/index.js
// 匯入 vue-router 資源
import { createRouter, createWebHashHistory } from 'vue-router';
// 匯入獨立元件
import Home from '../views/Home.vue';

// 路由表
const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/newpage',
    name: '新增頁面',
    component: () =&gt; import('../views/NewPage.vue'),
    children: &#91;
      {
        // 子項目的路徑不用加上/
        path: 'a',
        component: () =&gt; import('../views/ComponentA.vue'),
      },
      {
        path: 'b',
        component: () =&gt; import('../views/ComponentB.vue'),
      },
      // 參數 - 動態路由
      {
        path: 'dynamicrouter/:id',
        component: () =&gt; import('../views/DynamicRouter.vue'),
      },
      // 具名視圖(命名視圖)
      {
        path: 'namedview',
        component: () =&gt; import('../views/NamedView.vue'),
        children: &#91;
          {
            path: 'c2a',
            // 載入多個元件
            components: {
              // 命名視圖使用名稱就是物件所使用的名稱
              left: () =&gt; import('../views/ComponentC.vue'),
              right: () =&gt; import('../views/ComponentA.vue'),
            },
          },
          {
            path: 'a2b',
            components: {
              left: () =&gt; import('../views/ComponentA.vue'),
              right: () =&gt; import('../views/ComponentB.vue'),
            },
          },
        ],
      },
    ],
  },
];

// router 結構
const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

// 匯出
export default router;
</code></pre>



<p><a href="https://vuejs.org/api/compile-time-flags.html#vue-cli" target="_blank" rel="noreferrer noopener">Compile-Time Flags / vue-cli</a></p>



<pre class="wp-block-code"><code>// vue.config.js
module.exports = {
  chainWebpack: (config) =&gt; {
    config.plugin('define').tap((definitions) =&gt; {
      Object.assign(definitions&#91;0], {
        __VUE_OPTIONS_API__: 'true',
        __VUE_PROD_DEVTOOLS__: 'false',
        __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false'
      })
      return definitions
    })
  }
}
</code></pre>



<h3 class="wp-block-heading">動態路由搭配 Props</h3>



<p><a href="https://randomuser.me/" target="_blank" rel="noreferrer noopener">Random User 網站</a></p>



<ol class="wp-block-list">
<li>打開 DynamicRouter.vue 檔案<br>直接另存新檔 DynamicRouterByProps.vue</li>



<li>開啟 router/index.js 檔案<br>複製參數 – 動態路由的物件，並把對應的檔案調整</li>



<li>在網址列貼上新的路徑<br>http://localhost:8080/#/newpage/dynamicrouterbyprops/b9039789d7d5a209</li>



<li>在 router/index.js 檔案<br>在動態路由搭配 Props 物件裡面新增一個屬性 props<br>加上 id: seed 字串</li>



<li>在 DynamicRouterByProps.vue 檔案<br>做程式碼修改<br>新增 props 屬性，props 所取得的是 router/index.js 路由表所定義的 props 的 id<br>console 改成直接取得 props 的 id 內容<br>console.log(‘props’, this.id);</li>



<li>在 router/index.js 檔案<br>id 要改成使用動態路由的方式帶過來<br>調整程式碼 props 的地方，改成大括號直接 return 結果</li>
</ol>



<pre class="wp-block-code"><code>// 4. 在 router/index.js 檔案
props: () =&gt; ({
  // id: seed 字串
  id: 'b9039789d7d5a209',
}),
</code></pre>



<pre class="wp-block-code"><code>// 6. 在 router/index.js 檔案
props: (route) =&gt; {
  console.log('route:', route);
  return {
    id: route.params.id,
  };
},
</code></pre>



<pre class="wp-block-code"><code>// views/DynamicRouterByProps.vue
&lt;template&gt;
  &lt;div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
// 載入 axios 套件
import axios from 'axios';

export default {
  props: &#91;'id'],
  created() {
    // b9039789d7d5a209
    console.log('props', this.id);
    const seed = this.$route.params.id;
    axios.get(`https://randomuser.me/api/?seed=${seed}`)
      .then((res) =&gt; {
        console.log(res);
      });
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// router/index.js
// 匯入 vue-router 資源
import { createRouter, createWebHashHistory } from 'vue-router';
// 匯入獨立元件
import Home from '../views/Home.vue';

// 路由表
const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/newpage',
    name: '新增頁面',
    component: () =&gt; import('../views/NewPage.vue'),
    children: &#91;
      {
        // 子項目的路徑不用加上/
        path: 'a',
        component: () =&gt; import('../views/ComponentA.vue'),
      },
      {
        path: 'b',
        component: () =&gt; import('../views/ComponentB.vue'),
      },
      // 參數 - 動態路由
      {
        path: 'dynamicrouter/:id',
        component: () =&gt; import('../views/DynamicRouter.vue'),
      },
      // 動態路由搭配 Props
      {
        path: 'dynamicrouterbyprops/:id',
        component: () =&gt; import('../views/DynamicRouterByProps.vue'),
        props: (route) =&gt; {
          console.log('route:', route);
          return {
            id: route.params.id,
          };
        },
      },
      // 具名視圖(命名視圖)
      {
        path: 'namedview',
        component: () =&gt; import('../views/NamedView.vue'),
        children: &#91;
          {
            path: 'c2a',
            // 載入多個元件
            components: {
              // 命名視圖使用名稱就是物件所使用的名稱
              left: () =&gt; import('../views/ComponentC.vue'),
              right: () =&gt; import('../views/ComponentA.vue'),
            },
          },
          {
            path: 'a2b',
            components: {
              left: () =&gt; import('../views/ComponentA.vue'),
              right: () =&gt; import('../views/ComponentB.vue'),
            },
          },
        ],
      },
    ],
  },
];

// router 結構
const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

// 匯出
export default router;
</code></pre>



<h3 class="wp-block-heading">路由方法介紹</h3>



<p>使用 Options API 來操作路由</p>



<p><a href="https://next.router.vuejs.org/zh/api/#router-%E6%96%B9%E6%B3%95" target="_blank" rel="noreferrer noopener">路由方法</a>、<a href="https://next.router.vuejs.org/zh/api/#routelocationnormalized" target="_blank" rel="noreferrer noopener">路由屬性</a></p>



<ol class="wp-block-list">
<li>新增 RouterNavigation.vue 檔案、調整 router/index.js 檔案、調整 views/NewPage.vue 檔案</li>



<li>getRoute 取得屬性以及方法
<ul class="wp-block-list">
<li>$route – 取得目前在這個路由下有哪些的資訊<br>fullPath、params、<br>http://localhost:8080/#/newpage/routerNavigation?search=123</li>



<li>$router – 這個路由下可以使用的方法<br>常見方法切換頁面: push、replace</li>
</ul>
</li>



<li>push</li>



<li>replace</li>



<li>go</li>



<li>addRoute</li>
</ol>



<pre class="wp-block-code"><code>// 1. views/RouterNavigation.vue
&lt;template&gt;
  &lt;button type="button" @click="getRoute"&gt;getRoute&lt;/button&gt;

  &lt;button type="button" @click="push"&gt;Push&lt;/button&gt;
  &lt;button type="button" @click="replace"&gt;Replace&lt;/button&gt;
  &lt;button type="button" @click="go"&gt;Go&lt;/button&gt;
  &lt;hr&gt;
  &lt;button type="button" @click="addRoute"&gt;新增路由&lt;/button&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  methods: {
    // 包含歷史紀錄
    push() {

    },
    // 沒有歷史紀錄
    replace() {

    },
    // 操作歷史紀錄
    go() {

    },
    // 取得常用參數
    getRoute() {
      // 取得路由的屬性
      // https://next.router.vuejs.org/zh/api/#routelocationnormalized
      // 範例: this.$route.fullPath (目前網址)
      // console.log(this.$route);

      // 呼叫路由的方法
      // 參考: https://next.router.vuejs.org/zh/api/#router-方法
      // 範例: this.$router.go(-1) (回到前一頁)
      // console.log(this.$router);
    },

    // 延伸介紹
    addRoute() {
    
    },
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// 1. router/index.js
// 匯入 vue-router 資源
import { createRouter, createWebHashHistory } from 'vue-router';
// 匯入獨立元件
import Home from '../views/Home.vue';

// 路由表
const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/newpage',
    name: '新增頁面',
    component: () =&gt; import('../views/NewPage.vue'),
    children: &#91;
      {
        // 子項目的路徑不用加上/
        path: 'a',
        component: () =&gt; import('../views/ComponentA.vue'),
      },
      {
        path: 'b',
        component: () =&gt; import('../views/ComponentB.vue'),
      },
      // 參數 - 動態路由
      {
        path: 'dynamicrouter/:id',
        component: () =&gt; import('../views/DynamicRouter.vue'),
      },
      // 動態路由搭配 Props
      {
        path: 'dynamicrouterbyprops/:id',
        component: () =&gt; import('../views/DynamicRouterByProps.vue'),
        props: (route) =&gt; {
          console.log('route:', route);
          return {
            id: route.params.id,
          };
        },
      },
      {
        path: 'routernavigation',
        component: () =&gt; import('../views/RouterNavigation.vue'),
      },
      // 具名視圖(命名視圖)
      {
        path: 'namedview',
        component: () =&gt; import('../views/NamedView.vue'),
        children: &#91;
          {
            path: 'c2a',
            // 載入多個元件
            components: {
              // 命名視圖使用名稱就是物件所使用的名稱
              left: () =&gt; import('../views/ComponentC.vue'),
              right: () =&gt; import('../views/ComponentA.vue'),
            },
          },
          {
            path: 'a2b',
            components: {
              left: () =&gt; import('../views/ComponentA.vue'),
              right: () =&gt; import('../views/ComponentB.vue'),
            },
          },
        ],
      },
    ],
  },
];

// router 結構
const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

// 匯出
export default router;
</code></pre>



<pre class="wp-block-code"><code>// 1. views/NewPage.vue
&lt;template&gt;
  &lt;div class="row"&gt;
    &lt;div class="col-4"&gt;
      &lt;div class="list-group"&gt;
        &lt;router-link to="/newpage/a" class="list-group-item list-group-item-action"&gt;
          元件 A
        &lt;/router-link&gt;
        &lt;router-link to="/newpage/b" class="list-group-item list-group-item-action"&gt;
          元件 B
        &lt;/router-link&gt;
        &lt;router-link to="/newpage/namedview/c2a" class="list-group-item list-group-item-action"&gt;
          命名路由 c2a
        &lt;/router-link&gt;
        &lt;router-link to="/newpage/namedview/a2b" class="list-group-item list-group-item-action"&gt;
          命名路由 a2b
        &lt;/router-link&gt;
        &lt;router-link to="/newpage/dynamicrouter/b9039789d7d5a209"
        class="list-group-item list-group-item-action"&gt;
          動態路由($route)
        &lt;/router-link&gt;
        &lt;router-link to="/newpage/dynamicrouterbyprops/b9039789d7d5a209"
        class="list-group-item list-group-item-action"&gt;
          動態路由(props)
        &lt;/router-link&gt;
        &lt;router-link to="/newpage/routerNavigation" class="list-group-item list-group-item-action"&gt;
          路由導覽
        &lt;/router-link&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="col-8"&gt;
      &lt;router-view&gt;&lt;/router-view&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// views/RouterNavigation.vue
&lt;template&gt;
  &lt;button type="button" @click="getRoute"&gt;getRoute&lt;/button&gt;

  &lt;button type="button" @click="push"&gt;Push&lt;/button&gt;
  &lt;button type="button" @click="replace"&gt;Replace&lt;/button&gt;
  &lt;button type="button" @click="go"&gt;Go&lt;/button&gt;
  &lt;hr&gt;
  &lt;button type="button" @click="addRoute"&gt;新增路由&lt;/button&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  methods: {
    // 包含歷史紀錄
    push() {
      // 方法一: 直接帶入網址
      // this.$router.push('/newpage/dynamicrouter/b9039789d7d5a209');
      // 方法二: 使用 name 的方式帶入值
      this.$router.push({
        name: 'About',
      });
    },
    // 沒有歷史紀錄
    replace() {
      this.$router.replace({
        name: 'About',
      });
    },
    // 操作歷史紀錄
    go() {
      // 正整數 - 下一頁
      // 負整數 - 上一頁
      this.$router.go(-1);
    },
    // 取得常用參數
    getRoute() {
      // 取得路由的屬性
      // https://next.router.vuejs.org/zh/api/#routelocationnormalized
      // 範例: this.$route.fullPath (目前網址)
      // console.log(this.$route);

      // 呼叫路由的方法
      // 參考: https://next.router.vuejs.org/zh/api/#router-方法
      // 範例: this.$router.go(-1) (回到前一頁)
      console.log(this.$router);
    },
    // 延伸介紹
    addRoute() {
      this.$router.addRoute({
        path: '/newabout',
        name: 'newAbout',
        component: () =&gt; import('./About.vue'),
      });
    },
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">預設路徑以及重新導向</h3>



<p><a href="https://next.router.vuejs.org/guide/essentials/dynamic-matching.html#catch-all-404-not-found-route" target="_blank" rel="noreferrer noopener">重新導向說明</a></p>



<ol class="wp-block-list">
<li>新增 NotFound.vue 檔案</li>



<li>在 router/index.js 檔案<br>製作404頁面以及重新導向<br>404頁面提供一個頁面資訊給用戶了解到他現在進入到錯誤的路由，可以如何回到正確的路由下<br>重新導向直接把用戶導到一個正確的頁面</li>
</ol>



<pre class="wp-block-code"><code>// views/NotFound.vue
&lt;template&gt;
  &lt;h1&gt;404&lt;/h1&gt;
  &lt;p&gt;這頁找不到囉&lt;/p&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// router/index.js
// 匯入 vue-router 資源
import { createRouter, createWebHashHistory } from 'vue-router';
// 匯入獨立元件
import Home from '../views/Home.vue';

// 路由表
const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/newpage',
    name: '新增頁面',
    component: () =&gt; import('../views/NewPage.vue'),
    children: &#91;
      {
        // 子項目的路徑不用加上/
        path: 'a',
        component: () =&gt; import('../views/ComponentA.vue'),
      },
      {
        path: 'b',
        component: () =&gt; import('../views/ComponentB.vue'),
      },
      // 參數 - 動態路由
      {
        path: 'dynamicrouter/:id',
        component: () =&gt; import('../views/DynamicRouter.vue'),
      },
      // 動態路由搭配 Props
      {
        path: 'dynamicrouterbyprops/:id',
        component: () =&gt; import('../views/DynamicRouterByProps.vue'),
        props: (route) =&gt; {
          console.log('route:', route);
          return {
            id: route.params.id,
          };
        },
      },
      {
        path: 'routernavigation',
        component: () =&gt; import('../views/RouterNavigation.vue'),
      },
      // 具名視圖(命名視圖)
      {
        path: 'namedview',
        component: () =&gt; import('../views/NamedView.vue'),
        children: &#91;
          {
            path: 'c2a',
            // 載入多個元件
            components: {
              // 命名視圖使用名稱就是物件所使用的名稱
              left: () =&gt; import('../views/ComponentC.vue'),
              right: () =&gt; import('../views/ComponentA.vue'),
            },
          },
          {
            path: 'a2b',
            components: {
              left: () =&gt; import('../views/ComponentA.vue'),
              right: () =&gt; import('../views/ComponentB.vue'),
            },
          },
        ],
      },
    ],
  },
  // 404 頁面
  {
    path: '/:pathMatch(.*)*',
    component: () =&gt; import('../views/NotFound.vue'),
  },
  // 重新導向
  {
    path: '/newPage/:pathMatch(.*)*',
    redirect: {
      name: 'Home',
    },
  },
];

// router 結構
const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

// 匯出
export default router;
</code></pre>



<h3 class="wp-block-heading">路由設定選項</h3>



<p><a href="https://next.router.vuejs.org/zh/api/#routeroptions" target="_blank" rel="noreferrer noopener">路由選項</a></p>



<ol class="wp-block-list">
<li>在 App.vue 檔案程式碼調整<br>調整 Navbar固定在最上方、調整網頁的高度、在最下方增加一個 router-link</li>



<li>查看 Vue Router 文件<br>VueRouter &gt; RouterOptions</li>



<li>linkActiveClass</li>



<li>scrollBehavior</li>
</ol>



<pre class="wp-block-code"><code>// App.vue
&lt;template&gt;
  &lt;nav class="navbar navbar-expand-lg bg-body-tertiary fixed-top"&gt;
    &lt;div class="container-fluid"&gt;
      &lt;a class="navbar-brand" href="#"&gt;Navbar&lt;/a&gt;
      &lt;button class="navbar-toggler" type="button"
        data-bs-toggle="collapse" data-bs-target="#navbarNav"
        aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"&gt;
        &lt;span class="navbar-toggler-icon"&gt;&lt;/span&gt;
      &lt;/button&gt;
      &lt;div class="collapse navbar-collapse" id="navbarNav"&gt;
        &lt;ul class="navbar-nav"&gt;
          &lt;li class="nav-item"&gt;
            &lt;router-link to="/" class="nav-link active" aria-current="page"&gt;Home&lt;/router-link&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;router-link to="/about" class="nav-link"&gt;About&lt;/router-link&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;router-link :to="{ name: '新增頁面' }" class="nav-link"&gt;新增頁面&lt;/router-link&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;
  &lt;div class="container" style="height: 300vh"&gt;
    &lt;router-view/&gt;
  &lt;/div&gt;
  &lt;router-link to="/newpage/routernavigation"&gt;/newpage/routernavigation&lt;/router-link&gt;
&lt;/template&gt;

&lt;style lang="scss"&gt;
body {
  padding-top: 80px;
}
&lt;/style&gt;
</code></pre>



<pre class="wp-block-code"><code>// router/index.js
// 匯入 vue-router 資源
import { createRouter, createWebHashHistory } from 'vue-router';
// 匯入獨立元件
import Home from '../views/Home.vue';

// 路由表
const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.&#91;hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/newpage',
    name: '新增頁面',
    component: () =&gt; import('../views/NewPage.vue'),
    children: &#91;
      {
        // 子項目的路徑不用加上/
        path: 'a',
        component: () =&gt; import('../views/ComponentA.vue'),
      },
      {
        path: 'b',
        component: () =&gt; import('../views/ComponentB.vue'),
      },
      // 參數 - 動態路由
      {
        path: 'dynamicrouter/:id',
        component: () =&gt; import('../views/DynamicRouter.vue'),
      },
      // 動態路由搭配 Props
      {
        path: 'dynamicrouterbyprops/:id',
        component: () =&gt; import('../views/DynamicRouterByProps.vue'),
        props: (route) =&gt; {
          console.log('route:', route);
          return {
            id: route.params.id,
          };
        },
      },
      {
        path: 'routernavigation',
        component: () =&gt; import('../views/RouterNavigation.vue'),
      },
      // 具名視圖(命名視圖)
      {
        path: 'namedview',
        component: () =&gt; import('../views/NamedView.vue'),
        children: &#91;
          {
            path: 'c2a',
            // 載入多個元件
            components: {
              // 命名視圖使用名稱就是物件所使用的名稱
              left: () =&gt; import('../views/ComponentC.vue'),
              right: () =&gt; import('../views/ComponentA.vue'),
            },
          },
          {
            path: 'a2b',
            components: {
              left: () =&gt; import('../views/ComponentA.vue'),
              right: () =&gt; import('../views/ComponentB.vue'),
            },
          },
        ],
      },
    ],
  },
  // 404 頁面
  {
    path: '/:pathMatch(.*)*',
    component: () =&gt; import('../views/NotFound.vue'),
  },
  // 重新導向
  {
    path: '/newPage/:pathMatch(.*)*',
    redirect: {
      name: 'Home',
    },
  },
];

// router 結構
const router = createRouter({
  history: createWebHashHistory(),
  routes,
  linkActiveClass: 'active',
  scrollBehavior(to, from, savedPosition) {
    console.log(to, from, savedPosition);
    // `to` and `from` are both route locations
    // `savedPosition` can be null if there isn't one
    if (to.fullPath.match('newpage')) {
      return {
        top: 0,
      };
    }
    return {};
  },
});

// 匯出
export default router;</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Vue3 複習07</title>
		<link>/wordpress_blog/reviewvue3_07/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Mon, 18 Mar 2024 06:22:00 +0000</pubDate>
				<category><![CDATA[六角學院]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=830</guid>

					<description><![CDATA[Vue Cli，整合性工具開發好容易 Vue Cli 介紹 為什 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">Vue Cli，整合性工具開發好容易</h2>



<h3 class="wp-block-heading">Vue Cli 介紹</h3>



<h4 class="wp-block-heading">為什麼要用 Vue Cli</h4>



<ol class="wp-block-list">
<li>前端開發日趨複雜，每個開發者習慣亦有很大不同，因此<strong>整合性的工具可減少彼此開發上的差異</strong>。</li>



<li><strong>編譯環境越來越複雜</strong>，缺乏整合工具將會在每次專案都耗去大量時間。</li>



<li><strong>前後端分離形成主流</strong>，單頁式應用程式更符合開發習慣。</li>



<li>套件引用越來越多，導致難以管理。</li>
</ol>



<h4 class="wp-block-heading">Vue Cli 是什麼</h4>



<ol class="wp-block-list">
<li>基於 Webpack 所建置的開發工具</li>



<li>便於使用各種<strong>第三方套件</strong>&nbsp;(BS4, Vue Router…)</li>



<li>可運行&nbsp;<strong>Sass, Babel</strong>&nbsp;等編譯工具</li>



<li>獨特 .vue 檔案，一次包含 html, js, css</li>



<li>便於開發&nbsp;<strong>SPA</strong>&nbsp;的網頁工具</li>
</ol>



<h4 class="wp-block-heading">基於 Webpack 開發的 Vue Cli</h4>



<h4 class="wp-block-heading">整合了哪些開發環境?</h4>



<p>編譯各種語言使其可在瀏覽器上運行</p>



<p>程式碼品質檢視導入確保協作一致性</p>



<p>獨特 .vue 檔案開發 Vue 元件更為容易</p>



<h4 class="wp-block-heading">關於 .vue 檔案</h4>



<p>單一檔案同時包含 html, css, js 元件開發更一目了然</p>



<h4 class="wp-block-heading">SPA 又是什麼?</h4>



<p>原文為&nbsp;<strong>s</strong>ingle-<strong>p</strong>age&nbsp;<strong>a</strong>pplication，稱為<strong>單頁式應用程式</strong></p>



<h4 class="wp-block-heading">傳統的路由概念</h4>



<p>傳統後端路由 https://www.hexschool.com/<strong>user</strong></p>



<h4 class="wp-block-heading">SPA 的優點</h4>



<p>前端路由 https://www.hexschool.com/<strong>#/user</strong></p>



<h4 class="wp-block-heading">實現前後端分離</h4>



<h4 class="wp-block-heading">SPA 的優點</h4>



<p>前端路由 https://www.hexschool.com/<strong>#/order</strong></p>



<h4 class="wp-block-heading">關於 Cli</h4>



<p>原文為&nbsp;<strong>C</strong>ommand-<strong>L</strong>ine&nbsp;<strong>I</strong>nterface，簡單來說就是<strong>命令列介面</strong></p>



<h4 class="wp-block-heading">Vue 同時提供 GUI 的介面</h4>



<p>原文為&nbsp;<strong>G</strong>raphical&nbsp;<strong>U</strong>ser&nbsp;<strong>I</strong>nterface，簡單來說就是<strong>圖形使用者介面</strong></p>



<ul class="wp-block-list">
<li>減少許多指令的記憶</li>



<li>定義設定檔案</li>



<li>進行狀態輕鬆檢視</li>
</ul>



<h4 class="wp-block-heading">讓我們來開始使用 Vue Cli 吧</h4>



<p>準備流程:</p>



<ol class="wp-block-list">
<li>安裝 Node.js</li>



<li>安裝 Vue Cli 全域環境</li>



<li>建立 Vue 專案</li>



<li>開始開發!</li>
</ol>



<h3 class="wp-block-heading">Vue Cli 章節資源</h3>



<h4 class="wp-block-heading">安裝 Node.js</h4>



<p>參考 Gulp 課程，安裝流程的部分均可直接預覽(<a rel="noreferrer noopener" href="https://courses.hexschool.com/courses/gulp/lectures/11953830" target="_blank">1-3</a>,&nbsp;<a rel="noreferrer noopener" href="https://courses.hexschool.com/courses/gulp/lectures/11953831" target="_blank">1-4</a>)</p>



<p><a href="https://nodejs.org/en/" target="_blank" rel="noreferrer noopener">Nodejs 官方網站連結</a></p>



<h4 class="wp-block-heading">Vue Cli</h4>



<p><a rel="noreferrer noopener" href="https://cli.vuejs.org/" target="_blank">英文</a>、<a rel="noreferrer noopener" href="https://cli.vuejs.org/zh/guide/" target="_blank">中文</a></p>



<p>Vue Cli 4.x 同時可建立 Vue 2.x 及 Vue 3.x 的環境，無論你是要開發 2 or 3 都僅需要安裝當前版本的 Vue Cli 即可。</p>



<h4 class="wp-block-heading">其他相關連結</h4>



<p>以下為課程中會用到的相關連結</p>



<p><a href="https://next.router.vuejs.org/zh/index.html" target="_blank" rel="noreferrer noopener">Vue Router</a>、<a href="https://github.com/axios/axios" target="_blank" rel="noreferrer noopener">Axios</a>、<a href="https://www.npmjs.com/package/vue-axios" target="_blank" rel="noreferrer noopener">Vue Axios</a></p>



<h4 class="wp-block-heading">課程中運用到的指令</h4>



<p>建立專案 vue create {{ 專案名稱 }}</p>



<p>運行 Vue 開發環境 npm run serve</p>



<p>編譯 Production 版本 npm run build</p>



<h4 class="wp-block-heading">CLI 常用指令</h4>



<p>通用指令(Mac, Windows 共用)</p>



<h4 class="wp-block-heading">Nodejs 版本</h4>



<p>node -v</p>



<h4 class="wp-block-heading">npm 版本</h4>



<p>npm -v</p>



<h4 class="wp-block-heading">Windows 指令</h4>



<p>回到資料夾頂端 cd\\</p>



<p>回到上一層 cd..</p>



<p>進入資料夾路徑 cd {{ 資料夾路徑 }}</p>



<p>中斷目前操作 ctrl + c</p>



<h4 class="wp-block-heading">Mac OS 指令</h4>



<p>回到資料夾頂端 cd \\</p>



<p>回到上一層 cd ..</p>



<p>進入資料夾路徑 cd {{ 資料夾路徑 }}</p>



<p>中斷目前操作 ctrl + c</p>



<h3 class="wp-block-heading">安裝 Vue Cli 環境</h3>



<ul class="wp-block-list">
<li><a href="https://cli.vuejs.org/guide/installation.html" target="_blank" rel="noreferrer noopener">Vue Cli</a></li>
</ul>



<pre class="wp-block-code"><code>// 終端機
// 全域環境
// node.js 是運行環境
node -v
// npm 是套件的管理工具
npm -v
// 安裝 Vue Cli
npm install -g @vue/cli
// 查詢 Vue Cli 版本
vue -V
</code></pre>



<h4 class="wp-block-heading">建立專案</h4>



<ol class="wp-block-list">
<li>移動到指定資料夾位置<br>cd {{ 資料夾路徑 }}</li>



<li>創建一個項目 vue create<br>vue create new-project</li>



<li>Please pick a preset: (Use arrow Keys)<br>Manually select features</li>



<li>Check the features needed for your project: Choose Vue version, Babel, Router, CSS Pre-processors, Linter</li>



<li>Choose a version of Vue.js that you want to start the project with 3.x (Preview)</li>



<li>Use history mode for router? (Requires proper server setup for index fallback in production) No</li>



<li>Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)</li>



<li>Pick a linter / formatter config: Airbnb</li>



<li>Pick additional lint features: Lint on save</li>



<li>Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files</li>



<li>Save this as a preset for future projects? (y/N) n</li>
</ol>



<h4 class="wp-block-heading">打開專案</h4>



<ol class="wp-block-list">
<li>使用 VSCode 開啟專案</li>



<li>Ctrl + ` 打開終端機</li>



<li>使用 npm run serve 運行環境</li>



<li>點擊連結開啟網頁</li>
</ol>



<pre class="wp-block-code"><code>// 常用 NVM 指令
// 常見指令:
// nvm ls-remote: 列出目前可用的遠端 Node.js 版本
// nvm install: 安裝特定版本的 Node.js
// nvm ls: 列出本地端所安裝的 Node.js 環境
// nvm alias default node: 設定命令列預設開啟的 Node.js 版本
// nvm use: 當前命令列套用特定版本的 Node.js
</code></pre>



<h3 class="wp-block-heading">Vue Cli 環境中的檔案說明</h3>



<p>從下往上說明</p>



<ul class="wp-block-list">
<li>README.md – 介紹的檔案</li>



<li>package.json – 專案的配置檔案
<ul class="wp-block-list">
<li>“scripts” – 指令<br>執行指令，npm run serve、”serve” 滑鼠點擊 Run Script</li>
</ul>
</li>



<li>package-lock.json – 實際安裝的套件版本以及相關描述</li>



<li>babel.config.js – babel 設定檔案</li>



<li>.gitignore – 在 git 忽略的檔案</li>



<li>.eslintrc.js – eslint 設定檔案<br>依據選擇把設定檔以及延伸的規則載入</li>



<li>.editorconfig – 編輯器設定檔</li>



<li>.browserslistrc – 預期要支援的版本</li>



<li>src – 在這個專案中最重要的一個資料夾
<ul class="wp-block-list">
<li><strong>main.js – 整個專案的進入點</strong></li>
</ul>
</li>



<li>App.vue – 在 main.js 第一個生成的主要元件<br>通常只是做簡單的配置</li>



<li>views – 主要的頁面會放在這個資料夾內</li>



<li>router – 路由表放在 router 資料夾 index.js 檔案裡面</li>



<li>components – 掛載的子元件會放在 components 資料夾內</li>



<li>assets – 相關的一些其他資產<br>例如: 比較小的圖片、CSS、JS 檔案都可以放在這個資料夾內</li>



<li>public – 原則上是跟進入點沒有相關聯的檔案、不需要編譯的檔案<br>index.html，主要在 Vue 的元件生成之後要掛載在一個實體的頁面上所需要的檔案 (在 public 資料夾內唯一會編譯的檔案)<br>比較大的圖片或其他資源也可以放在 public 資料夾內</li>



<li>node_modules – 運行所需要的延伸套件<br>依據 package.json 檔案所列的清單安裝</li>
</ul>



<h3 class="wp-block-heading">指令太麻煩，來試試看 GUI 吧</h3>



<h4 class="wp-block-heading">建立專案</h4>



<ol class="wp-block-list">
<li>指令 vue ui</li>



<li>在 Vue 專案管理器按下新增</li>



<li>選擇專案路徑</li>



<li>在此新增專案</li>



<li>新增新專案
<ul class="wp-block-list">
<li>專案資料夾 gui-demo</li>



<li>包管理器 npm</li>



<li>Git 倉庫 初始化 git 倉庫(建議)</li>
</ul>
</li>



<li>模板<br>選擇一套模板 – 手動</li>



<li>功能<br>Choose Vue version、Babel、Router、CSS Pre-processors、Linter / Formatter</li>



<li>設定
<ul class="wp-block-list">
<li>Choose a version of Vue.js that you want to start the project with – 3.x (Preview)</li>



<li>Use history mode for router? (Requires proper server setup for index fallback in production) – No</li>



<li>Pick a CSS pr-processor: Sass/SCSS (with node-sass)</li>



<li>Pick a linter / formatter config: ESLint + Airbnb config</li>



<li>Pick additional lint features: Lint on save</li>
</ul>
</li>



<li>保存為新模板 – 新增專案，不保存預設</li>
</ol>



<h4 class="wp-block-heading">專案控制台</h4>



<ul class="wp-block-list">
<li>插件 – 加入新的 Vue CLI 插件</li>



<li>依賴 – 是用來管理包 (package)</li>



<li>設定 – 可以設定工具</li>



<li>任務 – 可以執行腳本 (例如 webpack)<br>serve 編譯和熱更新 (用於開發環境)<br>build 編譯並壓縮 (用於生產環境)</li>
</ul>



<h3 class="wp-block-heading">如何在 Cli 環境中加入 Vue 元件</h3>



<ol class="wp-block-list">
<li>安裝 Bootstrap 套件<br>終止終端機 – Ctrl + c，輸入 npm install bootstrap@5.3.3 安裝</li>



<li>重新運行 npm run serve</li>



<li>把 App.vue 檔案預設 CSS 樣式清除<br>匯入外部套件 – @import “bootstrap”;<br>載入 Bootstrap 元件測試</li>



<li>新增元件加到 About 頁面<br>到 Bootstrap Card 元件複製相對簡單的程式碼</li>



<li>新增的元件會放在 components 資料夾內<br>獨立的頁面會放在 views 資料夾內</li>



<li>在 components 資料夾內新增一個檔案 Card.vue<br>命名通常會使用開頭字母大寫的形式<br>使用 &lt;template&gt; 標籤把 Bootstrap Card 元件程式碼貼上</li>



<li>在 About.vue 加入卡片元件</li>



<li>在 Card.vue 加入 data</li>
</ol>



<pre class="wp-block-code"><code>// App.vue
&lt;template&gt;
  &lt;div id="nav"&gt;
    &lt;router-link to="/"&gt;Home&lt;/router-link&gt; |
    &lt;router-link to="/about"&gt;About&lt;/router-link&gt;
  &lt;/div&gt;
  &lt;router-view /&gt;
  &lt;button type="button" class="btn btn-primary"&gt;Primary&lt;/button&gt;
&lt;/template&gt;

&lt;style lang="scss"&gt;
@import "bootstrap";
&lt;/style&gt;
</code></pre>



<pre class="wp-block-code"><code>// Card.vue
&lt;template&gt;
  &lt;div class="card" style="width: 18rem"&gt;
    &lt;div class="card-body"&gt;
      &lt;h5 class="card-title"&gt;{{ title }}&lt;/h5&gt;
      &lt;h6 class="card-subtitle mb-2 text-body-secondary"&gt;Card subtitle&lt;/h6&gt;
      &lt;p class="card-text"&gt;
        Some quick example text to build on the card title and make up the bulk of the card's
        content.
      &lt;/p&gt;
      &lt;a href="#" class="card-link"&gt;Card link&lt;/a&gt;
      &lt;a href="#" class="card-link"&gt;Another link&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      title: '這是一段標題',
    };
  },
};
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// About.vue
&lt;template&gt;
  &lt;div class="about"&gt;
    &lt;h1&gt;This is an about page&lt;/h1&gt;
    &lt;!-- 7-4. Card 呈現在畫面上 --&gt;
    &lt;Card&gt;&lt;/Card&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
// 7-2. 外部元件匯入進來
import Card from '@/components/Card.vue';

// 7-1. 元件 js 匯出
export default {
  // 7-3. 區域註冊
  components: {
    Card,
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">Cli 中引入外部套件</h3>



<h4 class="wp-block-heading">VeeValidate</h4>



<p><a rel="noreferrer noopener" href="https://vee-validate.logaretm.com/v4/guide/overview/#using-a-package-manager" target="_blank">Getting Started – Using a package manager</a></p>



<p><a rel="noreferrer noopener" href="https://vee-validate.logaretm.com/v4/guide/global-validators/#vee-validaterules" target="_blank">Global Validators / @vee-validate/rules</a></p>



<p><a href="https://vee-validate.logaretm.com/v4/guide/i18n/#using-vee-validatei18n" target="_blank" rel="noreferrer noopener">Localization (i18n) / Using @vee-validate/i18n</a></p>



<ol class="wp-block-list">
<li>安裝 VeeValidate 套件</li>



<li>總共有三個套件必須要安裝<br>npm i vee-validate @vee-validate/rules @vee-validate/i18n –save</li>



<li>main.js 是 webpack 的主要進入點<br>外部資源匯入可以撰寫到 main.js 裡面</li>



<li>匯入 vee-validate 主套件、相關規則、多國語系的功能<br>import { Field, Form, ErrorMessage, defineRule, configure, } from ‘vee-validate’;<br>import { required, email, min } from ‘@vee-validate/rules’;<br>import { localize, setLocale } from ‘@vee-validate/i18n’;</li>



<li>基本設定<br>defineRule(‘required’, required);<br>defineRule(’email’, email);<br>defineRule(‘min’, min);<br>configure({<br>generateMessage: localize({ zh_TW: zhTW }),<br>validateOnInput: true,<br>});<br>setLocale(‘zh_TW’);</li>



<li>匯入繁體中文語系檔案<br>import zhTW from ‘@vee-validate/i18n/dist/locale/zh_TW.json’;</li>



<li>註冊元件- 程式碼改寫，註冊 Form, Field, ErrorMessage 元件<br>const app = createApp(App).use(router);<br>app.component(‘Form’, Form);<br>app.component(‘Field’, Field);<br>app.component(‘ErrorMessage’, ErrorMessage);<br><br>app.mount(‘#app’);</li>



<li>重新運行 npm run serve</li>



<li>表單驗證的表單加到畫面上加到 Home.vue &lt;template&gt; 標籤內，<br>並在 script 加上一些內容</li>
</ol>



<pre class="wp-block-code"><code>// main.js
import { createApp } from 'vue';

// 匯入 vee-validate 主套件
import {
  Field, Form, ErrorMessage, defineRule, configure,
} from 'vee-validate';
// 匯入 vee-validate 相關規則
import { required, email, min } from '@vee-validate/rules';
// 匯入多國語系的功能
import { localize, setLocale } from '@vee-validate/i18n';
// 匯入繁體中文語系檔案
import zhTW from '@vee-validate/i18n/dist/locale/zh_TW.json';

import App from './App.vue';
import router from './router';

// 定義驗證規則
defineRule('required', required);
defineRule('email', email);
defineRule('min', min);
// 設定 vee-validate 全域規則
configure({
  generateMessage: localize({ zh_TW: zhTW }), // 載入繁體中文語系
  validateOnInput: true, // 當輸入任何內容直接進行驗證
});
// 設定預設語系
setLocale('zh_TW');

const app = createApp(App).use(router);
// 註冊 vee-validate 三個全域元件
app.component('Form', Form);
app.component('Field', Field);
app.component('ErrorMessage', ErrorMessage);

app.mount('#app');
</code></pre>



<pre class="wp-block-code"><code>// Home.vue
&lt;template&gt;
  &lt;div class="home"&gt;
    &lt;img alt="Vue logo" src="../assets/logo.png"&gt;
    &lt;HelloWorld msg="Welcome to Your Vue.js App"/&gt;
    &lt;Form v-slot="{ errors, values, validate }" @submit="onSubmit"&gt;
      {{ errors }} {{ values }}
      &lt;div class="mb-3"&gt;
        &lt;label for="email" class="form-label"&gt;Email&lt;/label&gt;
        &lt;Field id="email" name="email" type="email"
        class="form-control" :class="{ 'is-invalid': errors&#91;'email'] }"
        placeholder="請輸入 Email" rules="email|required" v-model="user.email"&gt;&lt;/Field&gt;
        &lt;error-message name="email" class="invalid-feedback"&gt;&lt;/error-message&gt;
      &lt;/div&gt;
      &lt;button class="btn me-2 btn-outline-primary" type="button" @click="validate"&gt;驗證&lt;/button&gt;
      &lt;button class="btn btn-primary" type="submit"&gt;Submit&lt;/button&gt;
    &lt;/Form&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue';

export default {
  name: 'Home',
  components: {
    HelloWorld,
  },
  data() {
    return {
      user: {},
    };
  },
  methods: {
    onSubmit() {
      console.log(this.user);
    },
  },
  created() {
    console.log(this);
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">Vue Cli 中 VeeValidate 範例程式碼</h3>



<pre class="wp-block-code"><code>// main.js
import { createApp } from 'vue';

// 匯入 vee-validate 主套件
import {
  Field, Form, ErrorMessage, defineRule, configure,
} from 'vee-validate';
// 匯入 vee-validate 相關規則
import { required, email, min } from '@vee-validate/rules';
// 匯入多國語系的功能
import { localize, setLocale } from '@vee-validate/i18n';
// 匯入繁體中文語系檔案
import zhTW from '@vee-validate/i18n/dist/locale/zh_TW.json';

import App from './App.vue';
import router from './router';

// 定義驗證規則
defineRule('required', required);
defineRule('email', email);
defineRule('min', min);
// 設定 vee-validate 全域規則
configure({
  generateMessage: localize({ zh_TW: zhTW }), // 載入繁體中文語系
  validateOnInput: true, // 當輸入任何內容直接進行驗證
});
// 設定預設語系
setLocale('zh_TW');

const app = createApp(App).use(router);
// 註冊 vee-validate 三個全域元件
app.component('Form', Form);
app.component('Field', Field);
app.component('ErrorMessage', ErrorMessage);

app.mount('#app');
</code></pre>



<pre class="wp-block-code"><code>// Home.vue
&lt;template&gt;
  &lt;div class="home"&gt;
    &lt;Form v-slot="{ errors, values, validate }" @submit="onSubmit"&gt;
      {{ errors }} {{ values }}
      &lt;div class="mb-3"&gt;
        &lt;label for="email" class="form-label"&gt;Email&lt;/label&gt;
        &lt;Field id="email" name="email" type="email"
        class="form-control" :class="{ 'is-invalid': errors&#91;'email'] }"
        placeholder="請輸入 Email" rules="email|required" v-model="user.email"&gt;&lt;/Field&gt;
        &lt;error-message name="email" class="invalid-feedback"&gt;&lt;/error-message&gt;
      &lt;/div&gt;
      &lt;button class="btn me-2 btn-outline-primary" type="button" @click="validate"&gt;驗證&lt;/button&gt;
      &lt;button class="btn btn-primary" type="submit"&gt;Submit&lt;/button&gt;
    &lt;/Form&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;

export default {
  name: 'Home',
  data() {
    return {
      user: {},
    };
  },
  methods: {
    onSubmit() {
      console.log(this.user);
    },
  },
  created() {
    console.log(this);
  },
};
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">Vue Cli 中環境變數基礎觀念</h3>



<p>環境變數簡單區分為，development 開發者環境、production 正式環境。</p>



<ul class="wp-block-list">
<li><a href="https://cli.vuejs.org/zh/guide/mode-and-env.html#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F" target="_blank" rel="noreferrer noopener">環境變數</a></li>
</ul>



<h4 class="wp-block-heading">建立環境變數流程</h4>



<ol class="wp-block-list">
<li>在專案資料夾新增 .env 檔案<br>VUE_APP_NAME=小明的家</li>



<li>在 APP.vue 加上 &lt;script&gt; 程式碼</li>



<li>使用 {{ name }} 方式把 name 呈現到畫面上</li>



<li>調整環境變數後需重新運行 npm run serve</li>
</ol>



<pre class="wp-block-code"><code>// .env
VUE_APP_NAME=小明的家
</code></pre>



<pre class="wp-block-code"><code>// App.vue
&lt;template&gt;
  &lt;div id="nav"&gt;
    &lt;router-link to="/"&gt;Home&lt;/router-link&gt; |
    &lt;router-link to="/about"&gt;About&lt;/router-link&gt;
  &lt;/div&gt;
  &lt;router-view /&gt;
  {{ name }}
  &lt;button type="button" class="btn btn-primary"&gt;Primary&lt;/button&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      name: process.env.VUE_APP_NAME,
    };
  },
};
&lt;/script&gt;

&lt;style lang="scss"&gt;
@import "bootstrap";
&lt;/style&gt;
</code></pre>



<h3 class="wp-block-heading">Vue Cli 編譯設定檔</h3>



<ol class="wp-block-list">
<li>編譯專案 – npm run build<br>編譯完成後會產生 dist 資料夾</li>



<li>dist/index.html 檔案路徑是使用絕對路徑的方式進行撰寫，沒有透過 web server 的形式是沒辦法瀏覽</li>



<li>用 VSCode 把 dist 資料夾打開，使用 Go Live 打開</li>



<li>複製 .env 檔案改為 .env.production 檔案名稱</li>



<li>修改 .env.production 檔案內容<br>VUE_APP_NAME=老闆的家<br>重新編譯檔案 npm run build</li>



<li>再用 VSCode 把 dist 資料夾打開，使用 Go Live 打開<br>小明的家就變成老闆的家<br>環境變數在使用 production 之後的差異</li>
</ol>



<pre class="wp-block-code"><code>// .env.production
VUE_APP_NAME=老闆的家
</code></pre>



<h4 class="wp-block-heading">調整編譯後的路徑</h4>



<ol class="wp-block-list">
<li>新增 vue.config.js 檔案<br>建議直接使用 vue ui 建立 vue.config.js</li>



<li>在專案終端機輸入 vue ui<br>透過這種方式就不需要記太多的參數，就可以直接透過介面的形式來調整參數</li>



<li>設定 &gt; Vue CLI &gt; 公開路徑<br>公開路徑: /dist/，保存修改後就產生 vue.config.js 檔案</li>



<li>重新編譯專案 – npm run build<br>dist 路徑就可以直接在當前的專案下直接使用 web serve 的形式打開</li>



<li>直接選用 dist/index 檔案使用 Go Live 打開</li>
</ol>



<h4 class="wp-block-heading">哪些情況必須調整路徑</h4>



<ul class="wp-block-list">
<li>專案完成後把專案交給後端部署的時候</li>



<li>完成課程最終作業部屬部署到 GitHub Pages</li>
</ul>



<p>GitHub Pages 是一個免費的網頁空間，適合放置靜態網站。Vue Cli 編譯後的檔案就是屬於靜態網頁。</p>



<h3 class="wp-block-heading">Github Pages 教學連結</h3>



<ul class="wp-block-list">
<li><a href="https://youtu.be/njlABvVRB68?si=PUx9maAWm84p3GmI&amp;t=3574" target="_blank" rel="noreferrer noopener">GitHub Pages 教學影片</a>&nbsp;– 59:34 開始</li>
</ul>



<p>修正公開路徑: /專案資料夾名稱/dist/</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Vue3 複習06</title>
		<link>/wordpress_blog/reviewvue3_06/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Wed, 13 Mar 2024 03:22:00 +0000</pubDate>
				<category><![CDATA[六角學院]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=827</guid>

					<description><![CDATA[進階 API 進階章節說明 操作 DOM 元素技巧 refs 自 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">進階 API</h2>



<h3 class="wp-block-heading">進階章節說明</h3>



<h3 class="wp-block-heading">操作 DOM 元素技巧 refs</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;使用 ref 定義元素&lt;/h3&gt;
  &lt;input type="text" ref="inputDom" /&gt;

  &lt;h3&gt;使用 ref 取得元件內的資訊&lt;/h3&gt;
  &lt;button type="button" @click="getComponentInfo"&gt;取得元件資訊&lt;/button&gt;
  &lt;card ref="card"&gt;&lt;/card&gt;

  &lt;h3&gt;進階，使用 ref 搭配 Bootstrap&lt;/h3&gt;
  Bootstrap Modal:
  &lt;a href="https://getbootstrap.com/docs/5.0/components/modal/"
    &gt;https://getbootstrap.com/docs/5.0/components/modal/&lt;/a
  &gt;
  &lt;button @click="openModal"&gt;開啟 Bootstrap Modal&lt;/button&gt;
  &lt;!-- 2. 定義 ref modal 屬性 --&gt;
  &lt;div class="modal" tabindex="-1" ref="modal"&gt;
    &lt;div class="modal-dialog"&gt;
      &lt;div class="modal-content"&gt;
        &lt;div class="modal-header"&gt;
          &lt;h5 class="modal-title"&gt;Modal title&lt;/h5&gt;
          &lt;button
            type="button"
            class="btn-close"
            data-bs-dismiss="modal"
            aria-label="Close"
          &gt;&lt;/button&gt;
        &lt;/div&gt;
        &lt;div class="modal-body"&gt;
          &lt;p&gt;Modal body text goes here.&lt;/p&gt;
        &lt;/div&gt;
        &lt;div class="modal-footer"&gt;
          &lt;button
            type="button"
            class="btn btn-secondary"
            data-bs-dismiss="modal"
          &gt;
            Close
          &lt;/button&gt;
          &lt;button type="button" class="btn btn-primary"&gt;Save changes&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;!-- 另外載入 Bootstrap CDN --&gt;
&lt;script&gt;
  const app = Vue.createApp({
    data() {
      return {
        // 1. 先定義一個 bsModal 變數
        bsModal: "",
      };
    },
    methods: {
      getComponentInfo() {
        console.log(this.$refs.card);
        this.$refs.card.title = "被更新的元件標題";
      },
      openModal() {
        // 4. 加上 show() 把 bootstrap Modal 打開
        this.bsModal.show();
      },
    },
    mounted() {
      console.log(this.$refs, this.$refs.inputDom);
      this.$refs.inputDom.focus();

      // 3. 使用 this.bsModal 建立一個 new bootstrap Modal 實體
      // 3-1. this.refs 指到我們在畫面上這個的元素
      // 3-2. new bootstrap.Modal 實體會存在 this.bsModal
      // 3-3. this.bsModal 可以在不同作用域取用
      this.bsModal = new bootstrap.Modal(this.$refs.modal);
    },
  });

  app
    .component("card", {
      data() {
        return {
          title: "文件標題",
          content: "文件內文",
        };
      },
      template: `
                  &lt;div class="card" style="width: 18rem;"&gt;
                    &lt;div class="card-body"&gt;
                      &lt;h5 class="card-title"&gt;{{ title }}&lt;/h5&gt;
                      &lt;p class="card-text"&gt;{{ content }}&lt;/p&gt;
                    &lt;/div&gt;
                  &lt;/div&gt;
                `,
    })
    .mount("#app");
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">自訂元件生成位置 teleport</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;div id="target"&gt;&lt;/div&gt;

  &lt;h3&gt;Teleport 自訂義元件位置&lt;/h3&gt;
  結構：&lt;code&gt;&amp;lt;teleport to="{ target }"&amp;gt;&lt;/code&gt;
  &lt;card&gt;&lt;/card&gt;

  &lt;h3&gt;使用限制（錯誤情境）&lt;/h3&gt;
  &lt;card2&gt;&lt;/card2&gt;

  &lt;h3&gt;實用技巧（取代標題、多個）&lt;/h3&gt;
  &lt;new-title&gt;&lt;/new-title&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;script&gt;
  const app = Vue.createApp({});

  app.component("card", {
    data() {
      return {
        title: "文件標題",
        content: "文件內文",
        toggle: false,
      };
    },
    template: `
      &lt;div class="card" style="width: 18rem;"&gt;
        &lt;div class="card-body"&gt;
          &lt;h5 class="card-title"&gt;{{ title }}&lt;/h5&gt;
          &lt;p class="card-text"&gt;{{ content }}&lt;/p&gt;
          &lt;button type="button" @click="toggle = !toggle"&gt;切換元素顯示&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;teleport to="#target"&gt;
        &lt;div v-if="toggle" class="alert alert-danger"&gt;被招喚的元素&lt;/div&gt;
      &lt;/teleport&gt;
    `,
    props: &#91;"item"],
  });

  app.component("card2", {
    template: `
      &lt;teleport to=".col-md-3"&gt;
        &lt;div class="alert alert-danger"&gt;被招喚的元素&lt;/div&gt;
      &lt;/teleport&gt;
    `,
  });

  app.component("new-title", {
    template: `
      &lt;teleport to="title"&gt; - 新增的標題片段&lt;/teleport&gt;
      &lt;teleport to="h1"&gt; - 新增的文字片段&lt;/teleport&gt;
    `,
  });

  app.mount("#app");
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">跨層級資料傳遞 provide</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h2&gt;多層級資訊傳遞&lt;/h2&gt;
  &lt;ul&gt;
    &lt;li&gt;在外層加入 provide&lt;/li&gt;
    &lt;li&gt;內層元件補上 inject&lt;/li&gt;
  &lt;/ul&gt;
  &lt;card&gt;&lt;/card&gt;
  {{ user.name }}

  &lt;h3&gt;注意事項：響應式&lt;/h3&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;script&gt;
  // 最內層
  const userComponent = {
    template: `
      &lt;div&gt;
        userComponent 元件：&lt;br /&gt;
        {{ user.name }}, &lt;br /&gt;
        {{ user.uuid }}
      &lt;/div&gt;
    `,
    // 2. 內層元件補上 inject
    // 2-1. inject 是一個陣列
    // 2-2. inject 內容加上 user 這個名稱
    inject: &#91;"user"],
    created() {
      console.log(this.user);

      // 如果根元件沒有使用 function return 則不能使用響應式
      this.user.name = "杰倫";
    },
  };

  // 最外層
  const app = Vue.createApp({
    data() {
      return {
        user: {
          name: "小明",
          uuid: 78163,
        },
      };
    },
    // 1. 在外層加入 provide
    // 1-1 函式、物件運作上有點不一樣
    // provide: {
    //   user: {
    //     name: "小明",
    //     uuid: 78163,
    //   },
    // },

    // 注意事項: 響應式
    // provide 物件改成函式
    // 實戰建議直接使用函式的結構
    provide() {
      return {
        user: this.user,
      };
    },
  });

  // 內層
  app.component("card", {
    data() {
      return {
        title: "文件標題",
        content: "文件內文",
        toggle: false,
      };
    },
    components: {
      userComponent,
    },
    inject: &#91;"user"],
    template: `
      &lt;div class="card" style="width: 18rem;"&gt;
        &lt;div className="card-header"&gt;card 元件&lt;/div&gt;
        &lt;div class="card-body"&gt;
          {{ user.name }}
          &lt;userComponent&gt;&lt;/userComponent&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    `,
  });

  app.mount("#app");
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">元件直接加入 v-model</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h2&gt;將元件內的值傳回 v-model（update:modelValue）&lt;/h2&gt;
  &lt;p&gt;
    參考說明：&lt;a
      href="https://vue3js.cn/docs/zh/guide/component-custom-events.html#v-model-%E5%8F%82%E6%95%B0"
      &gt;https://vue3js.cn/docs/zh/guide/component-custom-events.html#v-model-%E5%8F%82%E6%95%B0&lt;/a
    &gt;
  &lt;/p&gt;
  {{ name }}
  &lt;!-- 1. v-model:text="name" --&gt;
  &lt;custom-input v-model:text="name"&gt;&lt;/custom-input&gt;

  &lt;hr /&gt;
  &lt;h2&gt;多個 v-model&lt;/h2&gt;
  {{ text }} {{ text2 }}
  &lt;!-- 1. v-model:t1="text"、v-model:t2="text2 --&gt;
  &lt;custom-input2 v-model:t1="text" v-model:t2="text2"&gt;&lt;/custom-input2&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;script&gt;
  const app = Vue.createApp({
    data() {
      return {
        name: "小明",

        text: "這是文字片段 1",
        text2: "這是文字片段 2",
      };
    },
  });

  // $emit('update:text', $event.target.value) 搭配 props，可以將更新後的值寫回 v-model 內
  app.component("custom-input", {
    props: &#91;"text"],
    // 2-1. :value="text"
    // 2-2. @input="$emit('update:text', $event.target.value)"
    template: `
    &lt;input type="text" 
    :value="text" 
    @input="$emit('update:text', $event.target.value)"
    class="form-control"&gt;
    `,
  });

  app.component("custom-input2", {
    props: &#91;"t1", "t2"],
    // 2-1 :value="t1"、:value="t2"
    // 2-2 @input="$emit('update:t1', $event.target.value)"
    //     @input="$emit('update:t2', $event.target.value)"
    template: `
    &lt;input type="text" 
    :value="t1"
    @input="$emit('update:t1', $event.target.value)"
    class="form-control"&gt;

    &lt;input type="text" 
    :value="t2" 
    @input="$emit('update:t2', $event.target.value)"
    class="form-control"&gt;
    `,
  });

  app.mount("#app");
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">混合元件方法 mixins</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h2&gt;單一混合、多個混合&lt;/h2&gt;
  &lt;card&gt;&lt;/card&gt;

  &lt;p&gt;重點：&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;可以重複混合&lt;/li&gt;
    &lt;li&gt;生命週期可以重複觸發&lt;/li&gt;
    &lt;li&gt;同名的變數、方法則會被後者覆蓋&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;script&gt;
  const mixComponent1 = {
    data() {
      return {
        name: "混合的元件",
      };
    },
    created() {
      console.log("混合的元件生命週期");
    },
  };
  const mixComponent2 = {
    data() {
      return {
        name: "混合的元件 2",
      };
    },
    created() {
      console.log("混合的元件生命週期 2");
    },
  };

  const app = Vue.createApp({});

  app.component("card", {
    // 優先順序
    // mixins 的優先順序是比較低的
    // 元件本身的優先順序會是比較高的
    data() {
      return {
        name: "card",
      };
    },
    template: `
      &lt;div class="card"&gt;
        &lt;div class="card-body"&gt;{{ name }}&lt;/div&gt;
      &lt;/div&gt;
    `,
    // 1-1. mixins 是一個陣列
    // 1-2. 陣列內容把其他元件的內容混合進來
    mixins: &#91;mixComponent1, mixComponent2],
    created() {
      console.log("card 的元件生命週期");
    },
  });

  app.mount("#app");
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">擴展元件方法 extend</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h2&gt;單一擴展&lt;/h2&gt;
  &lt;!-- &lt;card&gt;&lt;/card&gt; --&gt;

  &lt;h3&gt;權重&lt;/h3&gt;
  &lt;card2&gt;&lt;/card2&gt;

  &lt;h3&gt;重點：&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li&gt;擴展為單一擴展&lt;/li&gt;
    &lt;li&gt;生命週期可以與 mixins 重複觸發&lt;/li&gt;
    &lt;li&gt;權重：元件屬性 &gt; mixins &gt; extend&lt;/li&gt;
    &lt;li&gt;同名的變數、方法則會依據權重決定&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;script&gt;
  const extendComponent1 = {
    data() {
      return {
        name: "擴展的元件",
      };
    },
    created() {
      console.log("擴展的元件生命週期");
    },
  };
  const mixinComponent = {
    data() {
      return {
        name: "混合的元件",
      };
    },
    created() {
      console.log("混合的元件生命週期");
    },
  };

  const app = Vue.createApp({});

  app.component("card", {
    template: `
      &lt;div class="card"&gt;
        &lt;div class="card-body"&gt;{{ name }}&lt;/div&gt;
      &lt;/div&gt;
    `,
    // extends 後面直接加入一個物件，這物件是元件的本身
    extends: extendComponent1,
    created() {
      console.log("card 的元件生命週期");
    },
  });

  app.component("card2", {
    template: `
      &lt;div class="card"&gt;
        &lt;div class="card-body"&gt;{{ name }}&lt;/div&gt;
      &lt;/div&gt;
    `,
    data() {
      return {
        name: "元件資料狀態",
      };
    },
    mixins: &#91;mixinComponent],
    extends: extendComponent1,
    created() {
      console.log("card 的元件生命週期");
    },
  });

  app.mount("#app");
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">自定義指令 directive</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h2&gt;自訂義指令&lt;/h2&gt;
  &lt;h3&gt;用途&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li&gt;實戰中屬於進階功能，初學可先了解有此功能即可&lt;/li&gt;
    &lt;li&gt;可從延伸套件中看到相關的運用&lt;/li&gt;
    &lt;li&gt;多用於 HTML 上的便利操作，複雜功能還是會搭配元件&lt;/li&gt;
  &lt;/ul&gt;

  &lt;h3&gt;結構&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li&gt;v-{自訂義名稱}&lt;/li&gt;
    &lt;li&gt;
      主要透過生命週期來觸發變動，可參考：&lt;a
        href="https://vue3js.cn/docs/zh/guide/custom-directive.html#%E5%8A%A8%E6%80%81%E6%8C%87%E4%BB%A4%E5%8F%82%E6%95%B0"
        &gt;https://vue3js.cn/docs/zh/guide/custom-directive.html#%E5%8A%A8%E6%80%81%E6%8C%87%E4%BB%A4%E5%8F%82%E6%95%B0&lt;/a
      &gt;
    &lt;/li&gt;
  &lt;/ul&gt;
  &lt;!-- 1. v-validator --&gt;
  &lt;input type="email" v-model="text" v-validator="'form-control'" /&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;script type="module"&gt;
  const app = Vue.createApp({
    data() {
      return {
        text: "",
      };
    },
  });

  // 註冊指令
  app.directive("validator", {
    // directive 生命週期
    mounted(el, binding) {
      el.focus();

      // 將外部的值改為
      console.log(binding);
      el.className = binding.value;
    },
    updated: function (el, binding, vnode) {
      // el 元素本體
      // binding 綁定的資源狀態
      // vnode 虛擬 DOM 節點
      console.log("update", el, binding, vnode);
      const className = binding.value;

      // 尋找當前的 model 名稱（取得 key 值，並帶入第一個）
      const currentModel = Object.keys(binding.instance)&#91;0];
      // console.log(currentModel);

      // 從當前 Model 取值
      const value = binding.instance&#91;currentModel];
      console.log(currentModel, value);

      // Email validate
      // 正規表達式
      const re =
        /^((&#91;^&lt;&gt;()\&#91;\]\.,;:\s@\"]+(\.&#91;^&lt;&gt;()\&#91;\]\.,;:\s@\"]+)*)|(\".+\"))@((&#91;^&lt;&gt;()&#91;\]\.,;:\s@\"]+\.)+&#91;^&lt;&gt;()&#91;\]\.,;:\s@\"]{2,})$/i;

      if (!re.test(value)) {
        el.className = `${className} is-invalid`;
      } else {
        el.className = `${className} is-valid`;
      }
    },
  });

  app.mount("#app");
&lt;/script&gt;

&lt;script&gt;
  // ESM 版本的差異（需要 Webpack）
  // import { Field, Form, ErrorMessage } from 'vee-validate';
  //
  // export default {
  //   components: {
  //     Field,
  //     Form,
  //     ErrorMessage,
  //   },
  //   methods: {
  //     isRequired(value) {
  //       if (value &amp;&amp; value.trim()) {
  //         return true;
  //       }
  //
  //       return 'This is required';
  //     },
  //   },
  // };
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">擴充插件 plugins</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;p&gt;外部套件匯入方式&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;載入方式：使用 CDN 或使用 npm&lt;/li&gt;
    &lt;li&gt;
      運用方式：&lt;a href="https://www.npmjs.com/package/vue-axios"&gt;app.use()&lt;/a&gt;
      或
      &lt;a
        href="https://vee-validate.logaretm.com/v4/guide/components/validation#field-level-validation"
        &gt;元件的形式載入&lt;/a
      &gt;
      啟用。（另有指令等各種 Vue 的語法形式）
    &lt;/li&gt;
  &lt;/ul&gt;

  &lt;h3&gt;使用外部套件注意事項：&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li&gt;需多注意可搭配的版本號&lt;/li&gt;
    &lt;li&gt;更新頻率&lt;/li&gt;
    &lt;li&gt;使用人數&lt;/li&gt;
  &lt;/ul&gt;

  &lt;h3&gt;範例：載入 VeeValidate 驗證套件&lt;/h3&gt;
  &lt;!-- 1-1. form 改成 v-form --&gt;
  &lt;!-- 1-2. input 改成 v-field --&gt;
  &lt;!-- 2. v-form 加上 v-slot --&gt;

  &lt;v-form @submit="onSubmit" v-slot="{ errors }"&gt;
    {{ errors }}

    &lt;!-- 3-1. v-field 會加上規則 --&gt;
    &lt;!-- 3-4. name 也可以自定義名稱 --&gt;
    &lt;v-field
      name="欄位名稱"
      type="text"
      placeholder="Who are you"
      :rules="isRequired"
    &gt;&lt;/v-field&gt;
    &lt;!-- 3-5. error-message --&gt;
    &lt;!-- 3-6. 在 error-message 加上 name="欄位名稱" 對應到 v-field --&gt;
    &lt;error-message name="欄位名稱"&gt;請填寫此欄位&lt;/error-message&gt;

    &lt;button&gt;Submit&lt;/button&gt;
  &lt;/v-form&gt;

  &lt;p&gt;比對與 ESM 版本上的差異&lt;/p&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/vee-validate/4.1.17/vee-validate.min.js"&gt;&lt;/script&gt;
&lt;script type="module"&gt;
  console.log(VeeValidate);
  // VeeValidate CDN 版本
  const app = Vue.createApp({
    components: {
      // Components were renamed to avoid conflicts of HTML form element without a vue compiler
      VForm: VeeValidate.Form,
      VField: VeeValidate.Field,
      ErrorMessage: VeeValidate.ErrorMessage,
    },
    methods: {
      onSubmit(value) {
        // 3-7. 在 console 加上數值 1
        // 3-8. 在 onSubmit() 會自動帶上一個參數
        // console.log(1);
        console.log(value);
      },
      // 3-2. 規則會定義在方法裡面，參數會傳入一個值
      isRequired(value) {
        // 3-3. 驗證值有沒有填寫
        if (!value) {
          return "此欄是必填的";
        }
        return true;
      },
    },
  });

  app.mount("#app");
&lt;/script&gt;

&lt;script&gt;
  // ESM 版本的差異（需要 Webpack）
  // import { Field, Form, ErrorMessage } from 'vee-validate';
  //
  // export default {
  //   components: {
  //     Field,
  //     Form,
  //     ErrorMessage,
  //   },
  //   methods: {
  //     isRequired(value) {
  //       if (value &amp;&amp; value.trim()) {
  //         return true;
  //       }
  //
  //       return 'This is required';
  //     },
  //   },
  // };
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">表單驗證套件 vee-validation</h3>



<ul class="wp-block-list">
<li><a href="https://hackmd.io/FFv0a5cBToOATP7uI5COMQ" target="_blank" rel="noreferrer noopener">表單驗證套件補充資源</a></li>
</ul>



<h4 class="wp-block-heading">如何為單一表單(input) 進行驗證</h4>



<ol class="wp-block-list">
<li>加入 VeeValidation 相關資源</li>



<li>註冊元件
<ul class="wp-block-list">
<li>註冊全域的表單驗證元件 (VForm, VField, ErrorMessage)</li>
</ul>
</li>



<li>定義規則
<ul class="wp-block-list">
<li>選擇加入特定規則，全規則可<a href="https://vee-validate.logaretm.com/v4/guide/global-validators#vee-validaterules" target="_blank" rel="noreferrer noopener">參考</a></li>
</ul>
</li>



<li>加入多國語系
<ul class="wp-block-list">
<li>將<a href="https://github.com/logaretm/vee-validate/blob/vee-validate%404.1.16/packages/i18n/src/locale/zh_TW.json" target="_blank" rel="noreferrer noopener">外部資源</a>儲存至本地</li>
</ul>
</li>



<li>套用 v-form 並加入 v-slot</li>



<li>套用 v-field 及 error-message</li>



<li>加入自訂驗證、送出表單等行為</li>
</ol>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h2&gt;套用一個現成的流程&lt;/h2&gt;
  &lt;p&gt;
    參考文件：&lt;a href="https://hackmd.io/FFv0a5cBToOATP7uI5COMQ"
      &gt;https://hackmd.io/FFv0a5cBToOATP7uI5COMQ&lt;/a
    &gt;
  &lt;/p&gt;

  &lt;h3&gt;範例：載入 VeeValidate 驗證套件&lt;/h3&gt;
  &lt;!-- 5. 套用 v-form 並加入 v-slot --&gt;
  &lt;v-form @submit="onSubmit" v-slot="{ errors }"&gt;
    {{ errors }}
    &lt;div class="mb-3"&gt;
      &lt;label for="email" class="form-label"&gt;Email&lt;/label&gt;
      &lt;!-- 6. 套用 v-field 及 error-message --&gt;
      &lt;!-- 6-2. 規則加入 rules 是對應到先前所載入的規則 --&gt;
      &lt;!-- 6-3. 加上 input 樣式，使用 :class 方式 --&gt;
      &lt;!-- 8-1. 可以直接補上 v-model 對應 user.email 進行驗證 --&gt;
      &lt;v-field
        id="email"
        name="email"
        type="email"
        class="form-control"
        rules="email|required"
        v-model="user.email"
        placeholder="請輸入 Email"
        :class="{ 'is-invalid': errors&#91;'email'] }"
      &gt;&lt;/v-field&gt;
      &lt;!-- 6-1. error-message 必須對應到 v-field 的 name 欄位 --&gt;
      &lt;error-message name="email" class="invalid-feedback"&gt;&lt;/error-message&gt;
    &lt;/div&gt;

    &lt;div class="mb-3"&gt;
      &lt;label for="name" class="form-label"&gt;姓名&lt;/label&gt;
      &lt;v-field
        id="name"
        name="姓名"
        type="text"
        class="form-control"
        rules="required"
        v-model="user.name"
        placeholder="請輸入姓名"
        :class="{ 'is-invalid': errors&#91;'姓名']}"
      &gt;&lt;/v-field&gt;
      &lt;error-message name="姓名" class="invalid-feedback"&gt;&lt;/error-message&gt;
    &lt;/div&gt;

    &lt;div class="mb-3"&gt;
      &lt;label for="phone" class="form-label"&gt;電話&lt;/label&gt;
      &lt;!-- 7. 加入自訂驗證、送出表單等行為… --&gt;
      &lt;!-- 7-1. input 改成 v-field --&gt;
      &lt;!-- 7-2 規則加上 rules，這裡使用的是 :rules="isPhone" --&gt;
      &lt;!-- 綁定的是下方的方法 isPhone(value) --&gt;
      &lt;v-field
        id="phone"
        name="電話"
        type="text"
        class="form-control"
        :rules="isPhone"
        v-model="user.phone"
        placeholder="請輸入電話"
        :class="{ 'is-invalid': errors&#91;'電話']}"
      &gt;&lt;/v-field&gt;
      &lt;error-message name="電話" class="invalid-feedback"&gt;&lt;/error-message&gt;
    &lt;/div&gt;

    &lt;div class="mb-3"&gt;
      &lt;label for="region" class="form-label"&gt;地區&lt;/label&gt;
      &lt;v-field
        id="region"
        name="地區"
        class="form-control"
        :class="{ 'is-invalid': errors&#91;'地區']}"
        placeholder="請輸入地區"
        rules="required"
        v-model="user.region"
        as="select"
      &gt;
        &lt;option value=""&gt;請選擇地區&lt;/option&gt;
        &lt;option value="台北市"&gt;台北市&lt;/option&gt;
        &lt;option value="高雄市"&gt;高雄市&lt;/option&gt;
      &lt;/v-field&gt;
      &lt;error-message name="地區" class="invalid-feedback"&gt;&lt;/error-message&gt;
    &lt;/div&gt;

    &lt;div class="mb-3"&gt;
      &lt;label for="address" class="form-label"&gt;地址&lt;/label&gt;
      &lt;v-field
        id="address"
        name="地址"
        type="text"
        class="form-control"
        rules="required"
        v-model="user.address"
        placeholder="請輸入地址"
        :class="{ 'is-invalid': errors&#91;'地址']}"
      &gt;&lt;/v-field&gt;
      &lt;error-message name="地址" class="invalid-feedback"&gt;&lt;/error-message&gt;
    &lt;/div&gt;

    &lt;button class="btn btn-primary" type="submit"&gt;送出&lt;/button&gt;
  &lt;/v-form&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;!-- 1. 加入 VeeValidation 相關資源 --&gt;
&lt;!-- 主套件 --&gt;
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/vee-validate/4.1.17/vee-validate.min.js"&gt;&lt;/script&gt;
&lt;!-- i18n --&gt;
&lt;script src="https://cdn.jsdelivr.net/npm/@vee-validate/i18n@4.1.17/dist/vee-validate-i18n.min.js"&gt;&lt;/script&gt;
&lt;!-- rules --&gt;
&lt;script src="https://cdn.jsdelivr.net/npm/@vee-validate/rules@4.1.17/dist/vee-validate-rules.min.js"&gt;&lt;/script&gt;
&lt;script type="module"&gt;
  // 3. 定義規則
  // 選擇加入特定規則，全規則可參考
  // 參考: https://vee-validate.logaretm.com/v4/guide/global-validators#vee-validaterules
  VeeValidate.defineRule("email", VeeValidateRules&#91;"email"]);
  VeeValidate.defineRule("required", VeeValidateRules&#91;"required"]);

  // 4. 加入多國語系
  // 將外部資源儲存至本地
  // 外部資源: https://github.com/logaretm/vee-validate/blob/vee-validate%404.1.16/packages/i18n/src/locale/zh_TW.json
  // 讀取外部的資源
  VeeValidateI18n.loadLocaleFromURL("./zh_TW.json");

  // Activate the locale
  VeeValidate.configure({
    generateMessage: VeeValidateI18n.localize("zh_TW"), // 切換成中文版
    validateOnInput: true, // 調整為：輸入文字時，就立即進行驗證
  });

  const app = Vue.createApp({
    data() {
      return {
        // 8. 有加上資料欄位
        user: {
          email: "",
          name: "",
          address: "",
          phone: "",
          region: "",
        },
      };
    },
    methods: {
      onSubmit() {
        console.log(this.user);
      },
      isPhone(value) {
        const phoneNumber = /^(09)&#91;0-9]{8}$/;
        return phoneNumber.test(value) ? true : "需要正確的電話號碼";
      },
    },
    created() {
      console.log(this);
    },
  });

  // 2. 註冊元件
  // 註冊全域的表單驗證元件（VForm, VField, ErrorMessage）
  app.component("VForm", VeeValidate.Form);
  app.component("VField", VeeValidate.Field);
  app.component("ErrorMessage", VeeValidate.ErrorMessage);

  app.mount("#app");
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">進階 API 章節延伸資源</h3>



<p><a href="https://www.npmjs.com/package/axios" target="_blank" rel="noreferrer noopener">Vue Axios</a></p>



<p><a href="https://vee-validate.logaretm.com/v4/" target="_blank" rel="noreferrer noopener">Vee Validation v4 連結</a></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Vue3 複習05</title>
		<link>/wordpress_blog/reviewvue3_05/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Mon, 11 Mar 2024 12:14:00 +0000</pubDate>
				<category><![CDATA[六角學院]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=824</guid>

					<description><![CDATA[元件 元件介紹 為什麼要拆解成元件 頁面元件結構 元件資料獨立  [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">元件</h2>



<h3 class="wp-block-heading">元件介紹</h3>



<h4 class="wp-block-heading">為什麼要拆解成元件</h4>



<ul class="wp-block-list">
<li>增加程式碼的可複用性</li>



<li>避免單一檔案過大</li>



<li>易於管理及協作</li>



<li>元件功能獨立化</li>
</ul>



<h4 class="wp-block-heading">頁面元件結構</h4>



<h4 class="wp-block-heading">元件資料獨立 與 傳遞</h4>



<h4 class="wp-block-heading">接下來介紹到的 SPA 亦是使用元件</h4>



<h3 class="wp-block-heading">註冊元件的手法</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;元件基本範例及結構&lt;/h3&gt;
  &lt;p&gt;元件使用的基本要點&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;元件需要在 createApp 後，mount 前進行定義&lt;/li&gt;
    &lt;li&gt;元件需指定一個名稱&lt;/li&gt;
    &lt;li&gt;元件結構與最外層的根元件結構無異（除了增加 Template 的片段）&lt;/li&gt;
    &lt;li&gt;元件另有 prop, emits 等資料傳遞及事件傳遞&lt;/li&gt;
  &lt;/ul&gt;

  &lt;h3&gt;不同元件的註冊方式&lt;/h3&gt;

  &lt;h4&gt;全域註冊&lt;/h4&gt;
  &lt;p&gt;此 createApp 下，任何子元件都可運用，在中小型專案、一般頁面開發很方便&lt;/p&gt;
  &lt;alert1&gt;&lt;/alert1&gt;
  &lt;alert2&gt;&lt;/alert2&gt;

  &lt;h4&gt;區域註冊&lt;/h4&gt;
  &lt;p&gt;限制在特定元件下才可使用，在 Vue Cli 中很常使用此方法（便於管理）&lt;/p&gt;
  &lt;alert3&gt;&lt;/alert3&gt;
  &lt;alert4&gt;&lt;/alert4&gt;

  &lt;h4&gt;模組化&lt;/h4&gt;
  &lt;p&gt;同屬於區域註冊，Vue Cli 中的實戰運用技巧&lt;/p&gt;
  &lt;alert5&gt;&lt;/alert5&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;script type="module"&gt;
  // 注意，這段起手式與先前不同
  import alert5 from "./component-alert.js";

  // 區域註冊
  const alert3 = {
    data() {
      return {
        text: "內部文字 - 元件 3",
      };
    },
    template: `&lt;div class="alert alert-primary" role="alert"&gt;
                {{ text }}
              &lt;/div&gt;`,
  };

  const app = Vue.createApp({
    data() {
      return {
        text: "外部元件文字",
      };
    },
    // 區域註冊 - 掛載在根元件
    components: {
      alert3,
      alert5,
    },
    // 全域註冊 - 寫法 1
  }).component("alert1", {
    data() {
      return {
        text: "內部文字 - 元件 1",
      };
    },
    template: `&lt;div class="alert alert-primary" role="alert"&gt;
                {{ text }}
              &lt;/div&gt;`,
  });

  // 全域註冊 - 寫法 2
  app.component("alert2", {
    data() {
      return {
        text: "內部文字 - 元件 2",
      };
    },
    template: `&lt;div class="alert alert-primary" role="alert"&gt;
                {{ text }}
              &lt;/div&gt;`,
  });

  // 全域註冊
  app.component("alert4", {
    data() {
      return {
        text: "內部文字 - 元件 4",
      };
    },
    // 區域註冊 - 掛載在子元件
    components: {
      alert3,
    },
    template: `&lt;div class="alert alert-primary" role="alert"&gt;
                {{ text }}
                &lt;alert3&gt;&lt;/alert3&gt;
              &lt;/div&gt;`,
  });

  app.mount("#app");
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// component-alert.js
// 模組化
export default {
  data() {
    return {
      text: "外部匯入的元件 - 元件 5",
    };
  },
  template: `&lt;div class="alert alert-primary" role="alert"&gt;
    {{ text }}
  &lt;/div&gt;`,
};
</code></pre>



<h3 class="wp-block-heading">元件樣板製作</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;樣板建立方式&lt;/h3&gt;

  &lt;h4&gt;template&lt;/h4&gt;
  &lt;alert1&gt;&lt;/alert1&gt;

  &lt;h4&gt;x-template&lt;/h4&gt;
  &lt;alert2&gt;&lt;/alert2&gt;

  &lt;h4&gt;單文件元件（單一檔案包含 HTML, JS, CSS）&lt;/h4&gt;
  &lt;p&gt;
    本章節不介紹，在 Vue Cli 課程中將會實作（較為簡單，使用與 x-template 接近）
  &lt;/p&gt;

  &lt;hr /&gt;
  &lt;h3&gt;元件運用&lt;/h3&gt;
  &lt;h4&gt;直接使用 標籤 名稱&lt;/h4&gt;
  &lt;alert1&gt;&lt;/alert1&gt;

  &lt;h4&gt;搭配 v-for 也是沒問題的&lt;/h4&gt;
  &lt;alert1 v-for="i in array" :key="i"&gt;&lt;/alert1&gt;

  &lt;h4&gt;使用 v-is 綁定&lt;/h4&gt;
  &lt;!-- 要加入單引號 --&gt;
  &lt;div v-is="'alert1'"&gt;&lt;/div&gt;
  &lt;div is="vue:alert1"&gt;&lt;/div&gt;

  &lt;h4&gt;動態屬性&lt;/h4&gt;
  &lt;input type="text" v-model="componentName" /&gt;

  &lt;p&gt;任何標籤均可搭配 v-is 進行動態切換&lt;/p&gt;
  &lt;!-- This rule reports deprecated v-is directive in Vue.js v3.1.0+. --&gt;
  &lt;div v-is="componentName"&gt;&lt;/div&gt;

  &lt;p&gt;
    在 &lt;code&gt;component&lt;/code&gt; 標籤中，可以使用 is 縮寫（由 v2 版轉移過來的功能）
  &lt;/p&gt;
  &lt;component v-bind:is="componentName"&gt;&lt;/component&gt;
  &lt;component :is="componentName"&gt;&lt;/component&gt;

  &lt;h2&gt;動態標籤實戰技巧&lt;/h2&gt;
  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th&gt;標題&lt;/th&gt;
        &lt;th&gt;內文&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;!-- 會出現渲染顯示在不對的地方 --&gt;
      &lt;!-- &lt;table-row&gt;&lt;/table-row&gt; --&gt;
      &lt;!-- This rule reports deprecated v-is directive in Vue.js v3.1.0+. --&gt;
      &lt;tr v-is="'table-row'"&gt;&lt;/tr&gt;
      &lt;tr is="vue:table-row"&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;!-- x-template --&gt;
&lt;script type="text/x-template" id="alert-template"&gt;
  &lt;div class="alert alert-primary" role="alert"&gt;
    x-template 所建立的元件
  &lt;/div&gt;
&lt;/script&gt;

&lt;script type="module"&gt;
  // 注意，這段起手式與先前不同
  const app = Vue.createApp({
    data() {
      return {
        array: &#91;1, 2, 3],
        componentName: "alert1",
      };
    },
  });

  app.component("alert1", {
    template: `&lt;div class="alert alert-primary" role="alert"&gt;
                範例ㄧ
              &lt;/div&gt;`,
  });

  // x-template
  app.component("alert2", {
    template: "#alert-template",
  });

  app.component("table-row", {
    template: `&lt;tr&gt;
                &lt;td&gt;$&lt;/td&gt;
                &lt;td&gt;這是一個 tr 項目&lt;/td&gt;
              &lt;/tr&gt;`,
  });

  app.mount("#app");
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">Props 向內層元件傳遞資料狀態</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;Props 靜態資料&lt;/h3&gt;
  &lt;p&gt;由外部傳入資料至內部&lt;/p&gt;
  外部資源：https://images.unsplash.com/photo-1605784401368-5af1d9d6c4dc?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=600&amp;q=80
  &lt;photo
    url="https://images.unsplash.com/photo-1605784401368-5af1d9d6c4dc?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=600&amp;q=80"
  &gt;&lt;/photo&gt;

  &lt;h3&gt;動態資源&lt;/h3&gt;
  &lt;p&gt;技巧：前內、後外&lt;/p&gt;
  &lt;!-- v-bind:url="imgUrl" --&gt;
  &lt;photo :url="imgUrl"&gt;&lt;/photo&gt;

  &lt;h3&gt;單向數據流&lt;/h3&gt;
  &lt;photo2 :url="imgUrl"&gt;&lt;/photo2&gt;

  &lt;h3&gt;命名限制&lt;/h3&gt;
  &lt;!-- props 屬性使用小駝峰命名時 --&gt;
  &lt;!-- :superUrl 要改寫成 :super-url --&gt;
  &lt;photo3 :super-url="imgUrl"&gt;&lt;/photo3&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;script type="module"&gt;
  const app = Vue.createApp({
    data() {
      return {
        imgUrl:
          "https://images.unsplash.com/photo-1605784401368-5af1d9d6c4dc?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=600&amp;q=80",
      };
    },
  });

  app.component("photo", {
    props: &#91;"url"],
    template: `&lt;img :src="url" class="img-thumbnail" alt&gt;`,
  });

  app.component("photo2", {
    props: &#91;"url"],
    template: `&lt;img :src="url" class="img-thumbnail" alt&gt;&lt;br&gt;
    &lt;input type="text" v-model="url"&gt; {{ url }}`,
  });

  app.component("photo3", {
    // 小駝峰命名
    props: &#91;"superUrl"],
    template: `&lt;img :src="superUrl" class="img-thumbnail" alt&gt;`,
  });
  app.mount("#app");
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">元件型別驗證</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;Props 型別技巧&lt;/h3&gt;
  &lt;input type="number" v-model="money" /&gt;

  &lt;props-type money="300"&gt;&lt;/props-type&gt;
  &lt;props-type money="true"&gt;&lt;/props-type&gt;
  &lt;props-type :money="300"&gt;&lt;/props-type&gt;
  &lt;props-type :money="true"&gt;&lt;/props-type&gt;
  &lt;props-type :money="{}"&gt;&lt;/props-type&gt;
  &lt;props-type :money="money"&gt;&lt;/props-type&gt;
  &lt;props-type :money="boo"&gt;&lt;/props-type&gt;

  &lt;h3&gt;定義 Props 型別驗證&lt;/h3&gt;
  &lt;p&gt;實戰中不太會用到全部技巧，常用的有：&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;型別驗證&lt;/li&gt;
    &lt;li&gt;預設值、是否必填&lt;/li&gt;
  &lt;/ul&gt;
  &lt;props-validation :prop-a="fun" prop-c="required" :prop-f="10000"&gt;
  &lt;/props-validation&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;script type="module"&gt;
  const app = Vue.createApp({
    data() {
      return {
        money: 300,
        big: 100n,
        boo: true,
        fun: () =&gt; {
          return "a";
        },
      };
    },
  });

  app.component("props-type", {
    props: &#91;"money"],
    template: `&lt;div&gt;value: {{money}}, typeof:{{ typeof money }}&lt;/div&gt;`,
  });

  app.component("props-validation", {
    props: {
      // 單一型別檢查，可接受的型別 String, Number, Object, Boolean, Function(在 Vue 中可使用 Function 驗證型別)
      // null, undefined 會直接通過驗證
      propA: Function,
      // propA: String,

      // 多個型別檢查
      propB: &#91;String, Number],

      // 必要值
      propC: {
        type: String,
        required: true,
      },

      // 預設值
      propD: {
        type: Number,
        default: 300,
      },

      // 自訂函式
      propE: {
        type: Object,
        default() {
          return {
            money: 300,
          };
        },
      },

      // 自訂驗證
      propF: {
        validator(value) {
          return value &gt; 1000;
        },
      },
    },
    template: `
      &lt;p&gt;propA: {{ propA }}&lt;/p&gt;
      &lt;p&gt;propC: {{ propC }}&lt;/p&gt;
      &lt;p&gt;propD: {{ propD }}&lt;/p&gt;
      &lt;p&gt;propE: {{ propE }}&lt;/p&gt;
      &lt;p&gt;propF: {{ propF }}&lt;/p&gt;
      `,
  });

  app.mount("#app");
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">Emit 向外層傳遞事件</h3>



<pre class="wp-block-code"><code>// HTML
&lt;!-- 外層元件 --&gt;
&lt;div id="app"&gt;
  &lt;h3&gt;Emit 觸發外部事件&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li&gt;先定義外層接收的方法&lt;/li&gt;
    &lt;li&gt;定義內層的 $emit 觸發方法&lt;/li&gt;
    &lt;li&gt;使用 v-on 的方式觸發外層方法（口訣：前內、後外）&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p&gt;{{ num }}&lt;/p&gt;
  &lt;!-- 內層元件 --&gt;
  &lt;!-- 3. 使用 v-on 的方式觸發外層方法 (口訣: 前內、後外) --&gt;
  &lt;button-counter v-on:emit-num="addNum"&gt;&lt;/button-counter&gt;

  &lt;h3&gt;傳遞資料狀態&lt;/h3&gt;
  內部傳來的資料：{{ text }}&lt;br /&gt;
  &lt;!-- 內層元件 --&gt;
  &lt;button-text @emit-text="getData"&gt;&lt;/button-text&gt;

  &lt;h3&gt;命名注意&lt;/h3&gt;
  &lt;p&gt;駝峰的大寫文字，可以改為 `-` 進行串接&lt;/p&gt;
  內部傳來的文字：{{ text }}&lt;br /&gt;
  &lt;!-- 內層元件 --&gt;
  &lt;!-- 命名注意 @emitText 要改寫成 @emit-text --&gt;
  &lt;button-named @emit-text="getData"&gt;&lt;/button-named&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;script type="module"&gt;
  // 外層元件
  // 根元件
  const app = Vue.createApp({
    data() {
      return {
        num: 0,
        text: "",
      };
    },
    methods: {
      // 1. 先定義外層接收的方法
      addNum() {
        console.log("addNum", "先定義外層元件接收方法");
        this.num++;
      },
      // 1. 先定義外層接收的方法
      // 1-1. 透過參數的方式來接收內層傳來的資料
      getData(text) {
        console.log("getData", "先定義外層元件接收方法");
        this.text = text;
      },
    },
  });

  // 內層元件
  app.component("button-counter", {
    methods: {
      // 2. 定義內層的 $emit 觸發方法
      click() {
        console.log("click", "定義內層的 $emit 觸發方法");
        // 2-1. 自定義名稱
        this.$emit("emit-num");
      },
    },
    // 2-2. 事件觸發 v-on click
    template: `&lt;button type="button" @click="click"&gt;add&lt;/button&gt;`,
  });

  // 內層元件
  app.component("button-text", {
    data() {
      return {
        text: "內部資料",
      };
    },
    methods: {
      // 2. 定義內層的 $emit 觸發方法
      emit() {
        console.log("emit", "定義內層的 $emit 觸發方法");
        // 2-1. 自定義名稱, 要傳遞的參數
        this.$emit("emit-text", this.text);
      },
    },
    // 2-2. 事件觸發 v-on emit
    template: `&lt;button type="button" @click="emit"&gt;emit data&lt;/button&gt;`,
  });

  // 內層元件
  app.component("button-named", {
    methods: {
      // 2. 定義內層的 $emit 觸發方法
      emit() {
        console.log("emit", "定義內層的 $emit 觸發方法");
        // 2-1. 自定義名稱, 要傳遞的文字
        // 小駝峰命名
        this.$emit("emitText", "內部文字");
      },
    },
    // 2-2. 事件觸發 v-on emit
    template: `&lt;button type="button" @click="emit"&gt;emit data&lt;/button&gt;`,
  });

  app.mount("#app");
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">Emits 驗證</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;Emits API&lt;/h3&gt;
  {{ num }}
  &lt;button-counter @add="addNum"&gt;&lt;/button-counter&gt;

  &lt;h3&gt;驗證資料內容&lt;/h3&gt;
  &lt;button-counter2 @add="addNum"&gt;&lt;/button-counter2&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;script type="module"&gt;
  const app = Vue.createApp({
    data() {
      return {
        num: 0,
      };
    },
    methods: {
      addNum(num) {
        this.num = this.num + num;
      },
    },
  });

  app.component("button-counter", {
    data() {
      return {
        num: 1,
      };
    },
    // 主要會出現在該值是由 data 定義，但難以追蹤他的變化時會出現

    // 加上 emits 警告除錯
    emits: &#91;"add"],
    template: `
      &lt;button type="button" @click="num++"&gt;調整 num 的值&lt;/button&gt;
      &lt;button type="button" @click="$emit('add', num)"&gt;add&lt;/button&gt;`,
  });

  app.component("button-counter2", {
    emits: {
      add: (num) =&gt; {
        if (typeof num !== "number") {
          console.warn("add 事件參數型別需為 number");
        }
        return typeof num === "number";
      },
    },
    template: `
      &lt;button type="button" @click="$emit('add', '1')"&gt;Emit 驗證是否為數值&lt;/button&gt;
    `,
  });

  app.mount("#app");
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">Slot 插槽</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;Slot 插巢與插巢預設值&lt;/h3&gt;
  &lt;card&gt;
    &lt;p&gt;這是由外層定義的&lt;/p&gt;
  &lt;/card&gt;
  &lt;br /&gt;
  &lt;card&gt;&lt;/card&gt;

  &lt;h3&gt;具名插巢&lt;/h3&gt;
  &lt;card2&gt;
    &lt;template v-slot:header&gt;我喜歡這張卡片&lt;/template&gt;
    &lt;!--  預設請加入 default  --&gt;
    &lt;template v-slot:default&gt;這是卡片 2 號&lt;/template&gt;
    &lt;template v-slot:footer&gt;這是卡片腳&lt;/template&gt;
  &lt;/card2&gt;

  &lt;h3&gt;具名插巢縮寫&lt;/h3&gt;
  &lt;card2&gt;
    &lt;template #header&gt;我喜歡這張卡片&lt;/template&gt;
    &lt;!--  預設請加入 default  --&gt;
    &lt;template #default&gt;這是卡片 2 號&lt;/template&gt;
    &lt;template #footer&gt;這是卡片腳&lt;/template&gt;
  &lt;/card2&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;script type="module"&gt;
  const app = Vue.createApp({});
  app.component("card", {
    template: `&lt;div class="card" style="width: 18rem;"&gt;
      &lt;div class="card-header"&gt;
        元件 Header
      &lt;/div&gt;
      &lt;div class="card-body"&gt;
        &lt;slot&gt;
          &lt;p&gt;這是預設值&lt;/p&gt;
        &lt;/slot&gt;
      &lt;/div&gt;
      &lt;div class="card-footer"&gt;
        元件 Footer
      &lt;/div&gt;
    &lt;/div&gt;`,
  });

  app.component("card2", {
    template: `&lt;div class="card" style="width: 18rem;"&gt;
      &lt;div class="card-header"&gt;
        &lt;slot name="header"&gt;元件 Header&lt;/slot&gt;
      &lt;/div&gt;
      &lt;div class="card-body"&gt;
        &lt;slot&gt;這段是預設的文字&lt;/slot&gt;
      &lt;/div&gt;
      &lt;div class="card-footer"&gt;
        &lt;slot name="footer"&gt;元件 Footer&lt;/slot&gt;
      &lt;/div&gt;
    &lt;/div&gt;`,
  });

  app.mount("#app");
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">Slot Props 插槽傳遞資料狀態</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;插巢 Prop&lt;/h3&gt;
  &lt;p&gt;將元件內的變數取出使用，稱為 slot prop&lt;/p&gt;
  &lt;card&gt;
    &lt;template v-slot:default="slotProps"&gt;
      我想取出元件的值來使用 {{ slotProps.product.name }}
    &lt;/template&gt;
  &lt;/card&gt;
  &lt;hr /&gt;
  &lt;h2&gt;多個（解構）&lt;/h2&gt;
  {{ product }}
  &lt;card2 :product="product"&gt;
    &lt;template #header&gt; 買早餐 &lt;/template&gt;
    &lt;template #default="{ product, veganName }"&gt;
      {{ product }}
      &lt;br /&gt;
      {{ veganName }}
    &lt;/template&gt;
  &lt;/card2&gt;
  &lt;br /&gt;&lt;br /&gt;
  &lt;card3 :product="product"&gt;
    &lt;template #header&gt; 買早餐 &lt;/template&gt;
    &lt;template #default="{ product, veganName='是素非素' }"&gt;
      {{ product }}
      &lt;br /&gt;
      {{ veganName }}
    &lt;/template&gt;
  &lt;/card3&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;script type="module"&gt;
  const app = Vue.createApp({
    data() {
      return {
        product: {
          name: "蛋餅",
          price: 30,
          vegan: false,
        },
      };
    },
  });

  app.component("card", {
    data() {
      return {
        product: {
          name: "蛋餅",
          price: 30,
          vegan: false,
        },
      };
    },
    template: `
    &lt;div class="card" style="width: 18rem;"&gt;
      &lt;div class="card-body" &gt;
        &lt;slot :product="product"&gt;&lt;/slot&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    `,
  });

  app.component("card2", {
    props: &#91;"product"],
    data() {
      return {
        veganName: "",
      };
    },
    created() {
      console.log();
      this.veganName = this.product.vegan ? "素食" : "非素食";
    },
    template: `
    &lt;div class="card" style="width: 18rem;"&gt;
      &lt;div class="card-body" &gt;
        &lt;slot :product="product" :veganName="veganName"&gt;&lt;/slot&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    `,
  });

  app.component("card3", {
    props: &#91;"product"],
    data() {
      return {
        veganName: "",
      };
    },
    created() {
      console.log();
      this.veganName = this.product.vegan ? "素食" : "非素食";
    },
    template: `
    &lt;div class="card" style="width: 18rem;"&gt;
      &lt;div class="card-body" &gt;
        &lt;slot :product="product"&gt;&lt;/slot&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    `,
  });

  app.mount("#app");
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">Mitt 跨元件資料傳遞</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;p&gt;將元件內的變數取出使用，稱為 slot prop&lt;/p&gt;
  &lt;p&gt;
    套件路徑：&lt;a href="https://github.com/developit/mitt"
      &gt;https://github.com/developit/mitt&lt;/a
    &gt;
  &lt;/p&gt;
  &lt;card-on&gt;&lt;/card-on&gt;

  &lt;card-emit&gt;&lt;/card-emit&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;script type="module"&gt;
  import "https://unpkg.com/mitt/dist/mitt.umd.js"; // mitt
  const emitter = mitt();

  const app = Vue.createApp({});

  app.component("card-emit", {
    data() {
      return {
        product: {
          name: "蛋餅",
          price: 30,
          vegan: false,
        },
      };
    },
    methods: {
      sendData() {
        console.log("sendData");
        // 與 this.$emit 類似
        // 自定義的事件名稱, 參數
        emitter.emit("sendProduct", this.product);
      },
    },
    template: `
    &lt;div class="card" style="width: 18rem;"&gt;
      &lt;div class="card-body" &gt;
        &lt;button @click="sendData"&gt;送出&lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    `,
  });

  app.component("card-on", {
    data() {
      return {
        item: {},
      };
    },
    created() {
      emitter.on("sendProduct", (data) =&gt; {
        console.log("card-on", data);
        this.item = data;
      });
    },
    template: `
    &lt;div class="card" style="width: 18rem;"&gt;
      &lt;div class="card-body"&gt;
        {{ item }}
      &lt;/div&gt;
    &lt;/div&gt;
    `,
  });

  app.mount("#app");
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">元件章節作業</h3>



<p><a rel="noreferrer noopener" href="https://api.kcg.gov.tw/ServiceList/Detail/9c8e1450-e833-499c-8320-29b36b7ace5c" target="_blank">API 站點說明</a>、<a href="https://api.kcg.gov.tw/api/service/Get/9c8e1450-e833-499c-8320-29b36b7ace5c" target="_blank" rel="noreferrer noopener">API 連結位址</a>、<a href="https://github.com/hexschool/KCGTravel" target="_blank" rel="noreferrer noopener">備用位址</a>、<a href="https://codepen.io/Wcc723/pen/dyOgGvy?editors=1010" target="_blank" rel="noreferrer noopener">作業範例</a></p>



<h4 class="wp-block-heading">作業條件:</h4>



<ul class="wp-block-list">
<li>每張卡片都需要使用元件製作</li>



<li>分業需要製作成元件</li>



<li>分頁需要開收資料 props</li>



<li>分頁需要可以進行切換 emit</li>



<li>分頁細節</li>



<li>Previous, Next 需要可以正確運作 (套用 disabled)</li>



<li>當前頁面需要套用 .active 的視覺效果</li>
</ul>



<pre class="wp-block-code"><code>// HTML
&lt;!-- Bootstarp 5 CSS CDN --&gt;
&lt;link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
/&gt;
&lt;!-- Font Awesome --&gt;
&lt;link
  rel="stylesheet"
  href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css"
/&gt;
&lt;div id="app"&gt;
  &lt;div class="container py-5"&gt;
    &lt;h2&gt;元件章節作業&lt;/h2&gt;
    &lt;p&gt;作業條件:&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;每張卡片都需要使用元件製作&lt;/li&gt;
      &lt;li&gt;分頁需要製作成元件&lt;/li&gt;
      &lt;li&gt;分頁需要接收資料 props&lt;/li&gt;
      &lt;li&gt;分頁需要可以進行切換 emit&lt;/li&gt;
      &lt;li&gt;分頁細節&lt;/li&gt;
      &lt;li&gt;Previous, Next 需要可以正確運作 (套用 disabled)&lt;/li&gt;
      &lt;li&gt;當前頁面需要套用 .active 的視覺效果&lt;/li&gt;
    &lt;/ul&gt;
    &lt;div v-if="this.totalPages"&gt;
      &lt;div class="row" id="content"&gt;
        &lt;card :card-url="displayData"&gt;&lt;/card&gt;
      &lt;/div&gt;
      &lt;div class="d-flex justify-content-center mt-4"&gt;
        &lt;pagination
          :current-page="currentPage"
          :total-pages="totalPages"
          @emit-pagination="changePage"
        &gt;&lt;/pagination&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div v-else&gt;等待資料抓取中...&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;!-- Vue 3 CDN --&gt;
&lt;script src="https://unpkg.com/vue@3/dist/vue.global.js"&gt;&lt;/script&gt;
&lt;!-- Bootstrap 5 JS CDN --&gt;
&lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"&gt;&lt;/script&gt;
&lt;script type="module"&gt;
  const app = Vue.createApp({
    // 資料 (函式)
    data() {
      return {
        spotsData: &#91;],
        displayData: &#91;],
        perPage: 20,
        currentPage: 1,
        totalPages: 0,
      };
    },
    // 生命週期 (函式)
    created() {
      console.log(this);
      this.getData();
      this.pagination();
      this.updateData();
    },
    // 方法 (物件)
    methods: {
      getData() {
        const jsonUrl =
          "https://api.kcg.gov.tw/api/service/Get/9c8e1450-e833-499c-8320-29b36b7ace5c";
        let jsonData = {};

        fetch(jsonUrl, { method: "get" })
          .then((res) =&gt; {
            return res.json();
          })
          .then((data) =&gt; {
            jsonData = data.data.XML_Head.Infos.Info;
            // console.log(jsonData);
            this.spotsData = jsonData;
            this.pagination();
            this.updateData();
          });
      },
      pagination() {
        const dataTotal = this.spotsData.length;
        console.log(dataTotal);
        this.totalPages = Math.ceil(dataTotal / this.perPage);
      },
      changePage(page) {
        this.currentPage = page;
        this.updateData();
      },
      updateData() {
        const start = (this.currentPage - 1) * this.perPage;
        const end = start + this.perPage;
        this.displayData = this.spotsData.slice(start, end);
      },
    },
  });

  // 卡片元件
  app.component("card", {
    props: &#91;"cardUrl"],
    template: `
        &lt;div class="col-md-6 py-2" v-for="item in cardUrl" :key="item.Name"&gt;
            &lt;div class="card bg-dark text-white text-left"&gt;
              &lt;img class="card-img-top img-cover" height="155px" :src="item.Picture1"&gt;
              &lt;div class="card-img-overlay d-flex justify-content-between align-items-end p-0 px-3" style="background-color: rgba(0, 0, 0, .2)"&gt;
                &lt;h5 class="card-img-title-lg"&gt;{{ item.Name }}&lt;/h5&gt;
                &lt;h5 class="card-img-title-sm"&gt;{{ item.Zone }}&lt;/h5&gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;div class="card-body text-left"&gt;
                &lt;p class="card-text"&gt;&lt;i class="far fa-clock fa-clock-time"&gt;&lt;/i&gt;&amp;nbsp;
                  {{ item.Opentime }}&lt;/p&gt;
                &lt;p class="card-text"&gt;&lt;i class="fas fa-map-marker-alt fa-map-gps"&gt;&lt;/i&gt;&amp;nbsp;
                  {{ item.Add }}&lt;/p&gt;
                &lt;p class="card-text"&gt;&lt;i class="fas fa-mobile-alt fa-mobile"&gt;&lt;/i&gt;&amp;nbsp;
                  {{ item.Tel }}
                &lt;/p&gt;
                &lt;p class="card-text"&gt;&lt;i class="fas fa-tags text-warning"&gt;&lt;/i&gt;&amp;nbsp; {{ item.Ticketinfo ? item.Ticketinfo : '無' }}&lt;/p&gt;
            &lt;/div&gt;
        &lt;/div&gt;
        `,
  });

  // 分頁元件
  app.component("pagination", {
    props: &#91;"currentPage", "totalPages"],
    methods: {
      changePage(page) {
        this.$emit("emit-pagination", page);
      },
    },
    template: `
        &lt;nav aria-label="Page navigation example" v-if="this.totalPages"&gt;
          &lt;ul class="pagination" id="pageid"&gt;
            &lt;li class="page-item"&gt;
              &lt;a
                href="#"
                @click="changePage(currentPage - 1)"
                :class="{ disabled: currentPage === 1 }"
                class="page-link"
                &gt;Previous&lt;/a
              &gt;
            &lt;/li&gt;
            &lt;!--當被點擊時changePage會等於被點擊到的值--&gt;
            &lt;li class="page-item" v-for="i in totalPages" :key="i"&gt;
              &lt;a
                href="#"
                class="page-link"
                :class="{ active: currentPage === i }"
                @click="changePage(i)"
                &gt;{{i}}&lt;/a
              &gt;
            &lt;/li&gt;
            &lt;!--當cardsPage小於totalPages時，被點擊可以將頁碼加1--&gt;
            &lt;li class="page-item"&gt;
              &lt;a
                href="#"
                class="page-link"
                @click="changePage(currentPage + 1)"
                :class="{ disabled: currentPage === totalPages }"
                &gt;Next&lt;/a
              &gt;
            &lt;/li&gt;
          &lt;/ul&gt;
        &lt;/nav&gt;
        `,
  });

  app.mount("#app");
&lt;/script&gt;</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Vue3 複習04</title>
		<link>/wordpress_blog/reviewvue3_04/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Wed, 06 Mar 2024 02:34:00 +0000</pubDate>
				<category><![CDATA[六角學院]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=821</guid>

					<description><![CDATA[Options API: 方法、運算、監聽、生命週期 Optio [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">Options API: 方法、運算、監聽、生命週期</h2>



<h3 class="wp-block-heading">Options API 概述</h3>



<pre class="wp-block-code"><code>// Options API - HTML
&lt;div id="app"&gt;
  &lt;h3&gt;什麼是 options API 與 composition API&lt;/h3&gt;
  &lt;h5&gt;Option API Sample&lt;/h5&gt;
  &lt;button type="button" @click="trigger"&gt;{{ num }}&lt;/button&gt;

  &lt;h3&gt;常見的 Option API&lt;/h3&gt;
  &lt;p&gt;
    官網：&lt;a href="https://vue3js.cn/docs/zh/api/options-api.html"
      &gt;https://vue3js.cn/docs/zh/api/options-api.html&lt;/a
    &gt;
  &lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;常用功能(本章節介紹、部分在元件章節介紹)：data&lt;/li&gt;
    &lt;li&gt;元件知識：template(元件章節)、生命週期(本章節介紹)、資源&lt;/li&gt;
    &lt;li&gt;進階、外部套件：資源、組合、雜項(進階章節、套件運用)&lt;/li&gt;
  &lt;/ul&gt;

  &lt;h3&gt;提醒：option API 與 this&lt;/h3&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// Options API - JS
&lt;!-- https://vue3js.cn/docs/zh/api/options-api.html --&gt;
const App = {
  name: "漂亮的元件",
  data() {
    return {
      num: 0,
    };
  },
  methods: {
    trigger: function () {
      this.num++;
    },
  },
  created() {
    this.num = 1;
    console.log(this);
  },
};
Vue.createApp(App).mount("#app");
</code></pre>



<pre class="wp-block-code"><code>// Composition API - HTML
&lt;div id="compositionApp"&gt;
  &lt;h5&gt;Composition API Sample&lt;/h5&gt;
  &lt;button type="button" @click="trigger"&gt;{{ num }}&lt;/button&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// Composition API - JS
&lt;!-- https://vue3js.cn/docs/zh/api/composition-api.html#setup --&gt;
&lt;script type="module"&gt;
  import {
    ref,
    onMounted,
    createApp,
  } from "https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.5/vue.esm-browser.js";

  const CompositionApp = {
    setup() {
      // 定義資料
      const num = ref(0);
      // 生命週期
      onMounted(() =&gt; {
        num.value = 1;
      });
      // Methods
      function trigger() {
        console.log("trigger");
        num.value++;
      }
      // 匯出方法、資料
      return {
        trigger,
        num,
      };
    },
  };
  createApp(CompositionApp).mount("#compositionApp");
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">Methods 方法</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;methods 的結構&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li&gt;由 methods 定義的物件&lt;/li&gt;
    &lt;li&gt;內層均是函式&lt;/li&gt;
  &lt;/ul&gt;

  &lt;h3&gt;methods 的觸發方法（點擊、其它 options api、生命週期...）&lt;/h3&gt;
  &lt;button type="button" @click="trigger('Click Methods')"&gt;點擊觸發&lt;/button&gt;

  &lt;button type="button" @click="callOtherMethod"&gt;呼叫另一個 methods&lt;/button&gt;

  &lt;h3&gt;參數傳入&lt;/h3&gt;
  &lt;button type="button" @click="methodParameter(1, 2, 3, $event)"&gt;
    參數傳入
  &lt;/button&gt;

  &lt;h3&gt;使用 methods 處理複雜資料&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li v-for="product in products"&gt;
      {{ product.name }} / {{ product.price }}
      &lt;button type="button" @click="addToCart(product)"&gt;加入購物車&lt;/button&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
  ...
  &lt;h6&gt;購物車項目&lt;/h6&gt;
  &lt;ul&gt;
    &lt;li v-for="item in carts"&gt;{{ item.name }}&lt;/li&gt;
  &lt;/ul&gt;
  總金額 {{ sum }}

  &lt;h3&gt;作為 $filter 使用（取代複雜表達式）&lt;/h3&gt;
  {{ convertToAmount(sum) }}
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const App = {
  data() {
    return {
      num: 10,
      products: &#91;
        {
          name: "蛋餅",
          price: 30,
          vegan: false,
        },
        {
          name: "飯糰",
          price: 35,
          vegan: false,
        },
        {
          name: "小籠包",
          price: 60,
          vegan: false,
        },
        {
          name: "蘿蔔糕",
          price: 30,
          vegan: true,
        },
      ],
      carts: &#91;],
      sum: 0,
    };
  },
  methods: {
    trigger(name) {
      console.log(name, "此事件被觸發了");
    },
    callOtherMethod() {
      this.trigger("由 callOtherMethod 觸發");
    },
    methodParameter(a, b, c, d) {
      console.log(a, b, c, d);
    },
    addToCart(product) {
      // console.log(product);
      this.carts.push(product);
      this.calculate();
    },
    calculate() {
      // console.log("calculate");
      let total = 0;
      this.carts.forEach((item) =&gt; {
        total += item.price;
      });
      // console.log(total);
      this.sum = total;
    },
    convertToAmount(price) {
      return `NT$ ${price}`;
    },
  },
  created() {
    this.trigger("由生命週期觸發");
  },
};
Vue.createApp(App).mount("#app");
</code></pre>



<h3 class="wp-block-heading">Computed 運算基礎運用</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;Computed 將目前的數值運算呈現至畫面上&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li v-for="product in products"&gt;
      {{ product.name }} / {{ product.price }}
      &lt;button type="button" @click="addToCart(product)"&gt;加入購物車&lt;/button&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
  ...
  &lt;h6&gt;購物車項目&lt;/h6&gt;
  &lt;ul&gt;
    &lt;li v-for="item in carts"&gt;{{ item.name }}&lt;/li&gt;
  &lt;/ul&gt;
  total 的值： {{ total }}&lt;br /&gt;

  &lt;h3&gt;Computed 常見技巧 - 搜尋&lt;/h3&gt;
  &lt;input type="search" v-model="search" /&gt;
  &lt;ul&gt;
    &lt;li v-for="item in filterProducts"&gt;{{ item.name }} / {{ item.price }}&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const App = {
  data() {
    return {
      num: 10,
      search: "",
      products: &#91;
        {
          name: "蛋餅",
          price: 30,
          vegan: false,
        },
        {
          name: "飯糰",
          price: 35,
          vegan: false,
        },
        {
          name: "小籠包",
          price: 60,
          vegan: false,
        },
        {
          name: "蘿蔔糕",
          price: 30,
          vegan: true,
        },
      ],
      carts: &#91;],
      sum: 0,
    };
  },
  // 運算
  computed: {
    total() {
      // return `100`;
      let total = 0;
      this.carts.forEach((item) =&gt; {
        total += item.price;
      });
      return total;
    },
    filterProducts() {
      // return &#91;];
      return this.products.filter((item) =&gt; {
        // console.log(item);
        return item.name.match(this.search);
      });
    },
  },
  methods: {
    addToCart(product) {
      this.carts.push(product);
    },
  },
  created() {
    console.log(this);
  },
};
Vue.createApp(App).mount("#app");
</code></pre>



<h3 class="wp-block-heading">Computed 運算之 Getter, Setter</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;Computed 將目前的數值運算呈現至畫面上&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li v-for="product in products"&gt;
      {{ product.name }} / {{ product.price }}
      &lt;button type="button" @click="addToCart(product)"&gt;加入購物車&lt;/button&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
  ...
  &lt;h6&gt;購物車項目&lt;/h6&gt;
  &lt;ul&gt;
    &lt;li v-for="item in carts"&gt;{{ item.name }}&lt;/li&gt;
  &lt;/ul&gt;
  total 的值： {{ total }}&lt;br /&gt;

  &lt;h3&gt;Computed 常見技巧 - 搜尋&lt;/h3&gt;
  &lt;input type="search" v-model="search" /&gt;
  &lt;ul&gt;
    &lt;li v-for="item in filterProducts"&gt;{{ item.name }} / {{ item.price }}&lt;/li&gt;
  &lt;/ul&gt;

  &lt;h3&gt;Computed Getter, Setter&lt;/h3&gt;
  sum 的值：
  &lt;input type="number" v-model.number="num" /&gt;
  &lt;button type="button" @click="total = num"&gt;更新&lt;/button&gt;
  total 的值：{{ total }}&lt;br /&gt;
  sum 的值：{{ sum }}
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const App = {
  data() {
    return {
      num: 10,
      search: "",
      products: &#91;
        {
          name: "蛋餅",
          price: 30,
          vegan: false,
        },
        {
          name: "飯糰",
          price: 35,
          vegan: false,
        },
        {
          name: "小籠包",
          price: 60,
          vegan: false,
        },
        {
          name: "蘿蔔糕",
          price: 30,
          vegan: true,
        },
      ],
      carts: &#91;],
      sum: 0,
    };
  },
  // 運算
  computed: {
    total: {
      get() {
        let total = 0;
        this.carts.forEach((item) =&gt; {
          total += item.price;
        });
        // 講解1
        // return total;

        // 講解2
        return this.sum || total;

        // 練習
        // return (this.num = total);
      },
      set(val) {
        // 講解1
        // console.log(val);
        // this.sum = val * 0.8;

        // 講解2
        this.sum = val;

        // 練習
        // this.sum = val *0.8;
      },
    },
    filterProducts() {
      // return &#91;];
      return this.products.filter((item) =&gt; {
        // console.log(item);
        return item.name.match(this.search);
      });
    },
  },
  methods: {
    addToCart(product) {
      this.carts.push(product);
    },
  },
  created() {
    console.log(this);
  },
};
Vue.createApp(App).mount("#app");
</code></pre>



<h3 class="wp-block-heading">Watch 監聽</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;watch 監聽單一變數&lt;/h3&gt;
  &lt;label for="name"&gt;名字須超過十個字&lt;/label&gt;
  &lt;input type="text" id="name" v-model="tempName" /&gt;
  &lt;p&gt;result: {{ result }}&lt;/p&gt;
  &lt;p&gt;name: {{ name }}&lt;/p&gt;

  &lt;h3&gt;watch vs computed&lt;/h3&gt;
  &lt;h5&gt;Watch&lt;/h5&gt;
  &lt;ul&gt;
    &lt;li&gt;監聽單一 “變數” 觸發事件&lt;/li&gt;
    &lt;li&gt;該函式可同時操作多個變數&lt;/li&gt;
  &lt;/ul&gt;

  &lt;h5&gt;Computed&lt;/h5&gt;
  &lt;ul&gt;
    &lt;li&gt;監聽多個變數觸發事件&lt;/li&gt;
    &lt;li&gt;會產生一個值&lt;/li&gt;
  &lt;/ul&gt;

  &lt;label for="productName"&gt;商品名稱&lt;/label
  &gt;&lt;input type="text" v-model="productName" /&gt;&lt;br /&gt;
  &lt;label for="productPrice"&gt;商品價格&lt;/label
  &gt;&lt;input type="number" v-model.number="productPrice" /&gt;&lt;br /&gt;
  &lt;label&gt;&lt;input type="checkbox" v-model="productVegan" /&gt; 素食&lt;/label&gt;
  &lt;p&gt;Computed result2: {{ result2 }}&lt;/p&gt;
  &lt;p&gt;Watch result3: {{ result3 }}&lt;/p&gt;

  &lt;h3&gt;watch 深層監聽&lt;/h3&gt;
  &lt;label for="productName"&gt;商品名稱&lt;/label
  &gt;&lt;input type="text" v-model="product.name" /&gt;&lt;br /&gt;
  &lt;label for="productPrice"&gt;商品價格&lt;/label
  &gt;&lt;input type="number" v-model.number="product.price" /&gt;&lt;br /&gt;
  &lt;label&gt;&lt;input type="checkbox" v-model="product.vegan" /&gt; 素食&lt;/label&gt;
  &lt;p&gt;result4: {{ result4 }}&lt;/p&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const App = {
  data() {
    return {
      name: "",
      tempName: "",
      result: "",
      result3: "",
      result4: "",
      // 單一產品
      productName: "蛋餅",
      productPrice: 30,
      productVegan: false,
      // 單一產品
      product: {
        name: "蛋餅",
        price: 30,
        vegan: false,
      },
      products: &#91;
        {
          name: "蛋餅",
          price: 30,
          vegan: false,
        },
        {
          name: "飯糰",
          price: 35,
          vegan: false,
        },
        {
          name: "小籠包",
          price: 60,
          vegan: false,
        },
        {
          name: "蘿蔔糕",
          price: 30,
          vegan: true,
        },
      ],
      carts: &#91;],
      sum: 0,
    };
  },
  computed: {
    result2() {
      return `媽媽買了 ${this.productName}，總共花費 ${
        this.productPrice
      } 元，另外這 ${this.productVegan ? "是" : "不是"} 素食的`;
    },
  },
  // 監聽
  watch: {
    // 新的值 = n，舊的值 = o
    tempName(n, o) {
      console.log(n, o);
      if (n.length &gt;= 10) {
        this.result = `文字長度為 ${n.length} 個字，將儲存至變數中`;
        this.name = n;
      } else {
        this.result = `輸入的文字僅有 ${n.length} 個字，上一次有 ${o.length} 個字`;
      }
    },
    productName() {
      this.result3 = `媽媽買了 ${this.productName}，總共花費 ${
        this.productPrice
      } 元，另外這 ${this.productVegan ? "是" : "不是"} 素食的`;
    },
    productPrice() {
      this.result3 = `媽媽買了 ${this.productName}，總共花費 ${
        this.productPrice
      } 元，另外這 ${this.productVegan ? "是" : "不是"} 素食的`;
    },
    productVegan() {
      this.result3 = `媽媽買了 ${this.productName}，總共花費 ${
        this.productPrice
      } 元，另外這 ${this.productVegan ? "是" : "不是"} 素食的`;
    },
    product: {
      handler(n, o) {
        console.log(n, o);
        this.result4 = `媽媽買了 ${this.product.name}，總共花費 ${
          this.product.price
        } 元，另外這 ${this.product.vegan ? "是" : "不是"} 素食的`;
      },
      deep: true,
    },
  },
  // 保留文字：
  // `文字長度為 ${n.length} 個字，將儲存至變數中`
  // `輸入的文字僅有 ${n.length} 個字，上一次有 ${o.length} 個字`
  // `媽媽買了 ${this.productName}，總共花費 ${this.productPrice} 元，另外這 ${this.productVegan? '是' : '不是'} 素食的`
};
Vue.createApp(App).mount("#app");
</code></pre>



<h3 class="wp-block-heading">生命週期詳解</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;Vue 元件生命週期展示&lt;/h3&gt;
  &lt;p&gt;
    生命週期介紹：&lt;a
      href="https://vue3js.cn/docs/zh/guide/instance.html#%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E5%9B%BE%E7%A4%BA"
      &gt;生命週期&lt;/a
    &gt;
  &lt;/p&gt;
  &lt;button @click="isShowing = !isShowing" class="btn btn-primary"&gt;
    &lt;span v-if="isShowing"&gt;隱藏元件&lt;/span&gt;
    &lt;span v-else&gt;顯示元件&lt;/span&gt;
  &lt;/button&gt;
  &lt;hr /&gt;

  &lt;!-- &lt;child v-if="isShowing"&gt;&lt;/child&gt; --&gt;
  &lt;!-- &lt;child v-show="isShowing"&gt;&lt;/child&gt; --&gt;

  &lt;keep-alive&gt;
    &lt;child v-if="isShowing"&gt;&lt;/child&gt;
  &lt;/keep-alive&gt;

  &lt;p&gt;講解事項：&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;Vue 都是元件，元件的生命週期&lt;/li&gt;
    &lt;li&gt;生命週期流程&lt;/li&gt;
    &lt;li&gt;v-if 與 v-show 的差異&lt;/li&gt;
    &lt;li&gt;使用 Keep Alive 維持生命週期&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
&lt;script type="text/x-template" id="childarea"&gt;
  &lt;div&gt;
    &lt;h4&gt;{{ text }}&lt;/h4&gt;
    &lt;input type="text" class="form-control" v-model="text"&gt;
  &lt;/div&gt;
&lt;/script&gt;

&lt;script&gt;
  const child = {
    template: "#childarea",
    data: function () {
      return {
        text: "Vue data 資料狀態",
      };
    },
    beforeCreate() {
      console.log(`beforeCreate! ${this.text}`);
    },
    created() {
      console.log(`created! ${this.text}`);
      alert(`created! ${this.text}`);
    },
    beforeMount() {
      alert(`beforeMount! ${this.text}`);
    },
    mounted() {
      alert(`mounted! ${this.text}`);
    },
    updated() {
      console.log(`updated! ${this.text}`);
    },
    activated() {
      alert(`activated! ${this.text}`);
    },
    deactivated() {
      alert(`deactivated! ${this.text}`);
    },
    beforeUnmount() {
      console.log(`beforeUnmount! ${this.text}`);
    },
    unmounted() {
      console.log(`unmounted! ${this.text}`);
    },
  };
  const App = {
    data() {
      return {
        isShowing: false,
      };
    },
  };
  Vue.createApp(App).component("child", child).mount("#app");
&lt;/script&gt;
</code></pre>



<h3 class="wp-block-heading">Options API 章節作業</h3>



<h4 class="wp-block-heading">作業流程</h4>



<ol class="wp-block-list">
<li>使用生命週期取得遠端資料</li>



<li>點擊時，呈現右邊的區塊</li>



<li>搭配 Google Map 的 iframe 直接呈現位置</li>



<li>使用 computed 的技巧，製作過濾功能</li>



<li>使用 watch，製作瀏覽紀錄 (數量不超過十筆)</li>
</ol>



<pre class="wp-block-code"><code>// HTML
&lt;!-- 
  作業流程
  1. 使用生命週期取得遠端資料
  2. 點擊時，呈現右邊的區塊
  3. 搭配 Google Map 的 iframe 直接呈現位置
  4. 使用 computed 的技巧，製作過濾功能
  5. 使用 watch，製作瀏覽紀錄 (數量不超過十筆)
--&gt;

&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"&gt;&lt;/script&gt;
&lt;div id="app" class="mt-2"&gt;
  &lt;div class="row h-100"&gt;
    &lt;div class="col-md-3 h-100 d-flex flex-column"&gt;
      &lt;div class="form-floating mb-2"&gt;
        &lt;input type="search" class="form-control" id="search" placeholder="search" v-model="cacheSearch"&gt;
        &lt;label for="search"&gt;search&lt;/label&gt;
      &lt;/div&gt;
      &lt;div class="list-group option"&gt;
        &lt;!-- datastore 改成 filterSearch --&gt;
        &lt;!-- 'data' 改成 filter --&gt;
        &lt;label class="list-group-item" v-for="(item, key) in filterSearch" :key="'filter' + key"&gt;
          &lt;input class="form-check-input me-1" type="radio" :value="item" name="area" @click="getArea(item)" :checked="cacheArea.Name === item.Name"&gt;
          {{ item.Name }}
        &lt;/label&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="col-md-8 h-100 d-flex flex-column"&gt;
      &lt;div class="form-floating"&gt;
        &lt;select id="cacheArea" class="form-select w-50 mb-2" aria-label="select example" v-model="cacheArea"&gt;
          &lt;option selected value="" disabled&gt;瀏覽紀錄&lt;/option&gt;
          &lt;option v-for="(item, key) in browseLog" :value="item"&gt;{{ key + 1 }}. {{ item.Name }}&lt;/option&gt;
        &lt;/select&gt;
        &lt;label for="cacheArea"&gt;瀏覽紀錄&lt;/label&gt;
      &lt;/div&gt;
      &lt;div class="card overflow-auto"&gt;
        &lt;!-- v-if、v-else --&gt;
        &lt;div v-if="cacheArea.Name"&gt;
          &lt;img :src="cacheArea.Picture1" class="card-img-top" :alt="cacheArea.Name"&gt;
          &lt;iframe width="100%" height="300" frameborder="0" scrolling="no" marginheight="0" marginwidth="0"
            :src=`https://maps.google.com.tw/maps?f=q&amp;hl=zh-TW&amp;geocode=&amp;q=${cacheArea.Py},${cacheArea.Px}(${cacheArea.Name})&amp;z=16&amp;output=embed`&gt;
          &lt;/iframe&gt;
          &lt;div class="card-body"&gt;
            &lt;h5 class="card-title"&gt;{{ cacheArea.Name }}&lt;/h5&gt;
            &lt;p&gt;{{ cacheArea.Description }}&lt;/p&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div v-else class="card-body"&gt;
          請選擇地方
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const apiUrl = 'https://raw.githubusercontent.com/hexschool/KCGTravel/master/datastore_search.json';

axios.get(apiUrl).then((res) =&gt; {
  // 取得遠端資料
})

const App = {
  data() {
    return {
      // datastore 物件改成陣列
      datastore: &#91;],
      cacheArea: '',
      cacheSearch: '',
      browseLog: &#91;],
    };
  },
  methods: {
    getData() {
      const apiUrl = 'https://raw.githubusercontent.com/hexschool/KCGTravel/master/datastore_search.json';

      axios.get(apiUrl).then((res) =&gt; {
        console.log(res, '抓取資料成功');
        this.datastore = res.data.result.records;
        // console.log(this.datastore);
      }).catch((err) =&gt; {
        console.log(err, '抓取資料失敗');
      })
    },
    getArea(area) {
      console.log(area, '取得地點');
      this.cacheArea = area;
    }
  },
  created() {
    this.getData();
  },
  computed: {
    filterSearch() {
      return this.datastore.filter((item) =&gt; item.Name.match(this.cacheSearch));
    }
  },
  watch: {
    cacheArea() {
      if (this.browseLog.length &lt; 10) {
        this.browseLog.push(this.cacheArea);
      } else {
        this.browseLog.shift();
        this.browseLog.push(this.cacheArea);
      }
    }
  }
};

Vue.createApp(App).mount('#app');
</code></pre>



<pre class="wp-block-code"><code>// CSS
#app {
  height: 600px;
}

.option {
  overflow-y: auto;
}</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Vue3 複習03</title>
		<link>/wordpress_blog/reviewvue3_03/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Fri, 01 Mar 2024 09:25:00 +0000</pubDate>
				<category><![CDATA[六角學院]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=818</guid>

					<description><![CDATA[指令語法全介紹 &#124; 操作畫面超容易 指令觀念介紹 綁定內容於畫面 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">指令語法全介紹 | 操作畫面超容易</h2>



<h3 class="wp-block-heading">指令觀念介紹</h3>



<h3 class="wp-block-heading">綁定內容於畫面上 v-text</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;字串 v-text 與 &lt;code v-pre&gt;{{}}&lt;/code&gt; (Mustache)&lt;/h3&gt;
  &lt;p&gt;{{ name }}在{{ position }}吃早餐&lt;/p&gt;
  &lt;p&gt;&lt;strong v-text="name"&gt;&lt;/strong&gt;在&lt;span v-text="position"&gt;&lt;/span&gt;吃早餐&lt;/p&gt;
  &lt;input type="text" v-model="name"&gt;

  &lt;hr&gt;
  &lt;h3&gt;原始字串 v-html&lt;/h3&gt;
  &lt;div v-html="rawHtml"&gt;&lt;/div&gt;
  &lt;p&gt;&lt;a href="https://v3.cn.vuejs.org/api/directives.html#v-html"&gt;注意事項&lt;/a&gt;&lt;/p&gt;

  &lt;hr&gt;
  &lt;h3&gt;單次綁定 v-once&lt;/h3&gt;
  &lt;p v-once&gt;{{ name }}在{{ position }}吃早餐&lt;/p&gt;
  &lt;input type="text" v-model="name"&gt;

  &lt;hr&gt;
  &lt;h3&gt;進階技巧：表達式&lt;/h3&gt;
  &lt;p&gt;樣板字面值: {{ `${name}在${position}吃早餐` }}&lt;/p&gt;
  &lt;p&gt;反轉字串: {{ text.split('').reverse().join('') }}&lt;/p&gt;
  &lt;p&gt;綁定 methods: {{ say('杰倫') }}&lt;/p&gt;
  &lt;p&gt;JS 運算: {{ 1 + 1 }}&lt;/p&gt;

  &lt;hr&gt;
  &lt;h3&gt;進階技巧：顯示資料狀態&lt;/h3&gt;
  &lt;p&gt;顯示目前的陣列內容 {{ products }}&lt;/p&gt;

  &lt;hr&gt;
  &lt;h3&gt;顯示 &lt;span v-pre&gt;{{ }}&lt;/span&gt;&lt;/h3&gt;
  &lt;p v-pre&gt;這段文字不會被轉譯：{{ name }}在{{ position }}吃早餐&lt;/p&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const App = {
  name: 'Hexschool Component',
  data() {
    return {
      name: '小明',
      position: '早餐店',
      text: '小明在早餐店吃早餐',
      rawHtml: '&lt;p&gt;小明在早餐店吃早餐&lt;/p&gt;',
      products: &#91;
        {
          name: '蛋餅',
          price: 30,
          vegan: false
        },
        {
          name: '飯糰',
          price: 35,
          vegan: false
        },
        {
          name: '小籠包',
          price: 60,
          vegan: false
        },
        {
          name: '蘿蔔糕',
          price: 30,
          vegan: true
        }
      ],
      selected: &#91;
        {
          name: '蛋餅',
          price: 30,
          vegan: false
        },
        {
          name: '飯糰',
          price: 35,
          vegan: false
        },
      ]
    }
  },
  methods: {
    say(name) {
      return `${name}在${this.position}吃早餐`
    }
  },
  computed: {
    total() {
      const total = this.selected.reduce((curr, next) =&gt; {
        return curr + next.price;
      }, 0);
      console.log(total);
      return total;
    }
  },
  created() {

  }
};

Vue.createApp(App).mount('#app');
</code></pre>



<h3 class="wp-block-heading">多筆資料渲染 v-for</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;呈現多筆資料於畫面上 v-for&lt;/h3&gt;
  &lt;p&gt;菜單&lt;/p&gt;
  &lt;ul&gt;
    &lt;li v-for="(item, key) in products" &gt;
      {{ key + 1 }} - {{ item.name }} / {{ item.price }}
    &lt;/li&gt;
  &lt;/ul&gt;

  &lt;h3&gt;物件回圈&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li v-for="(item, key) in productsObj"&gt;
      {{ key }} - {{ item.name }} / {{ item.price }}元
    &lt;/li&gt;
  &lt;/ul&gt;

  &lt;h3&gt;純數值回圈&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li v-for="i in 5"&gt;{{ i }}&lt;/li&gt;
  &lt;/ul&gt;

  &lt;h3&gt;v-for 與 key&lt;/h3&gt;
  &lt;p&gt;菜單&lt;/p&gt;
  &lt;ul&gt;
    &lt;li v-for="(item, key) in products" v-bind:key="item.name"&gt;
      {{ key }} - {{ item.name}} / {{ item.price }} 元
      &lt;input type="text"&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
  &lt;p&gt;說明：有相同父元素的子元素必須有獨特的 key。重複的 key 會造成渲染錯誤。&lt;/p&gt;
  &lt;button type="button" v-on:click="reverseArray"&gt;反轉資料內容&lt;/button&gt;

  &lt;hr&gt;
  &lt;h3&gt;進階技巧：在 template 標籤使用 v-for&lt;/h3&gt;
  &lt;table class="table"&gt;
    &lt;tbody&gt;
      &lt;template v-for="(item, i) in products" v-bind:key="item.name"&gt;
        &lt;tr&gt;
          &lt;th rowspan="2"&gt;{{ i + 1 }}&lt;/th&gt;
          &lt;td colspan="2"&gt;
            {{ item.name }}
          &lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
          &lt;td&gt;
            {{ item.price }}元
          &lt;/td&gt;
          &lt;td&gt;
            &lt;!-- 三元運算子 --&gt;
            &lt;!-- 變數 ? true : false --&gt;
            {{ item.vegan ? "素食" : "不可素食" }}
          &lt;/td&gt;
        &lt;/tr&gt;
      &lt;/template&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;p&gt;&lt;a href="https://cn.vuejs.org/guide/essentials/list.html#v-for-on-template"&gt;參考介紹&lt;/a&gt;&lt;/p&gt;

  &lt;h3&gt;補充說明：v-for 與元件&lt;/h3&gt;
  &lt;ul&gt;
    &lt;list-item :item="item" v-for="(item, key) in products" :key="item.name + 2"&gt;&lt;/list-item&gt;
  &lt;/ul&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const App = {
  data() {
    return {
      name: '小明',
      products: &#91;
        {
          name: '蛋餅',
          price: 30,
          vegan: false
        },
        {
          name: '飯糰',
          price: 35,
          vegan: false
        },
        {
          name: '小籠包',
          price: 60,
          vegan: false
        },
        {
          name: '蘿蔔糕',
          price: 30,
          vegan: true
        },
      ],
      productsObj: {
        chineseOmelette: {
          name: '蛋餅',
          price: 30,
          vegan: false
        },
        riceBall: {
          name: '飯糰',
          price: 35,
          vegan: false
        },
        soupDumpling: {
          name: '小籠包',
          price: 60,
          vegan: false
        },
        radishCake: {
          name: '蘿蔔糕',
          price: 30,
          vegan: true
        }
      },
    }
  },
  methods: {
    reverseArray: function () {
      this.products.reverse();
    },
  },
};

Vue.createApp(App)
  .component('list-item', {
    template: `
      &lt;li&gt;
        {{ item.name}} / {{ item.price }} 元
      &lt;/li&gt;
    `,
    props: &#91;'item']
  }).mount('#app');
</code></pre>



<h3 class="wp-block-heading">條件判斷 v-if</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;v-if&lt;/h3&gt;
  &lt;p v-if="isFull"&gt;小明 飽了&lt;/p&gt;
  &lt;p v-else&gt;小明 還沒吃飽&lt;/p&gt;
  &lt;!-- {{ isFull }} --&gt;
  &lt;button type="button" v-on:click="change('isFull')"&gt;狀態切換&lt;/button&gt;

  &lt;hr /&gt;
  &lt;h3&gt;v-else-if&lt;/h3&gt;
  &lt;nav class="nav nav-pills nav-fill"&gt;
    &lt;a
      class="nav-link"
      href="#"
      v-bind:class="{ 'active': link === '小明' }"
      v-on:click="link = '小明'"
      &gt;小明&lt;/a
    &gt;
    &lt;a
      class="nav-link"
      href="#"
      v-bind:class="{ 'active': link === '小美' }"
      v-on:click="link = '小美'"
      &gt;小美&lt;/a
    &gt;
    &lt;a
      class="nav-link"
      href="#"
      v-bind:class="{ 'active': link === '杰倫' }"
      v-on:click="link = '杰倫'"
      &gt;杰倫&lt;/a
    &gt;
  &lt;/nav&gt;
  &lt;div&gt;
    &lt;!-- {{ link }} --&gt;
    &lt;div v-if="link === '小明'"&gt;小明吃早餐&lt;/div&gt;
    &lt;div v-else-if="link === '小美'"&gt;小美去百貨公司&lt;/div&gt;
    &lt;div v-else-if="link === '杰倫'"&gt;杰倫去幫助人&lt;/div&gt;
  &lt;/div&gt;

  &lt;hr /&gt;
  &lt;h3&gt;注意事項：v-for 與 v-if 混用&lt;/h3&gt;
  &lt;ul&gt;
    &lt;template v-for="(item, key) in products" v-bind:key="item.name"&gt;
      &lt;li v-if="item.price &lt;= 35"&gt;{{ item.name}} / {{ item.price }} 元&lt;/li&gt;
    &lt;/template&gt;
  &lt;/ul&gt;
  &lt;p&gt;
    參考說明：&lt;a
      href="https://vue3js.cn/docs/zh/guide/conditional.html#v-if-%E4%B8%8E-v-for-%E4%B8%80%E8%B5%B7%E4%BD%BF%E7%94%A8"
      &gt;https://vue3js.cn/docs/zh/guide/conditional.html#v-if-%E4%B8%8E-v-for-%E4%B8%80%E8%B5%B7%E4%BD%BF%E7%94%A8&lt;/a
    &gt;
  &lt;/p&gt;

  &lt;hr /&gt;
  &lt;h3&gt;v-if 與 v-show&lt;/h3&gt;
  &lt;p v-show="isFull"&gt;小明 飽了&lt;/p&gt;
  &lt;p v-if="isFull"&gt;小明 飽了&lt;/p&gt;
  &lt;button type="button" v-on:click="change('isFull')"&gt;狀態切換&lt;/button&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const App = {
  data() {
    return {
      name: "小明",
      isFull: true,
      link: "小明",
      products: &#91;
        {
          name: "蛋餅",
          price: 30,
          vegan: false,
        },
        {
          name: "飯糰",
          price: 35,
          vegan: false,
        },
        {
          name: "小籠包",
          price: 60,
          vegan: false,
        },
        {
          name: "蘿蔔糕",
          price: 30,
          vegan: true,
        },
      ],
      productsObj: {
        chineseOmelette: {
          name: "蛋餅",
          price: 30,
          vegan: false,
        },
        riceBall: {
          name: "飯糰",
          price: 35,
          vegan: false,
        },
        soupDumpling: {
          name: "小籠包",
          price: 60,
          vegan: false,
        },
        radishCake: {
          name: "蘿蔔糕",
          price: 30,
          vegan: true,
        },
      },
    };
  },
  methods: {
    change: function (key) {
      this&#91;key] = !this&#91;key];
    },
  },
};

Vue.createApp(App).mount("#app");
</code></pre>



<h3 class="wp-block-heading">HTML 屬性綁定 v-bind</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;綁定屬性 v-bind&lt;/h3&gt;
  &lt;p&gt;{{ breakfastShop.name }}&lt;/p&gt;
  &lt;img
    v-bind:src="breakfastShop.imgUrl"
    class="square-img"
    v-bind:title="breakfastShop.name"
    alt=""
  /&gt;

  &lt;h3&gt;縮寫形式 &lt;code&gt;:&lt;/code&gt;&lt;/h3&gt;
  &lt;img
    :src="breakfastShop.imgUrl"
    class="square-img"
    :title="breakfastShop.name"
    alt=""
  /&gt;

  &lt;hr /&gt;
  &lt;h3&gt;更多屬性綁定&lt;/h3&gt;
  小明還想點餐：
  &lt;form action=""&gt;
    &lt;input type="text" value="我要吃蘿蔔糕" /&gt;
    &lt;button type="submit" :disabled="isFull"&gt;送出&lt;/button&gt;
  &lt;/form&gt;

  &lt;button type="button" v-on:click="change('isFull')"&gt;狀態切換&lt;/button&gt;

  &lt;hr /&gt;
  &lt;h3&gt;搭配 v-for&lt;/h3&gt;
  &lt;table class="table"&gt;
    &lt;tbody&gt;
      &lt;tr v-for="item in products" :key="item.imgUrl"&gt;
        &lt;td&gt;
          &lt;img :src="item.imgUrl" class="square-img" alt="" /&gt;
        &lt;/td&gt;
        &lt;td&gt;{{ item.name }}&lt;/td&gt;
        &lt;td&gt;{{ item.price }}元&lt;/td&gt;
        &lt;td&gt;
          &lt;div class="form-check"&gt;
            &lt;input
              class="form-check-input"
              type="checkbox"
              value=""
              :id="item.name"
            /&gt;
            &lt;label class="form-check-label" :for="item.name"&gt; 我要這個 &lt;/label&gt;
          &lt;/div&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

  &lt;hr /&gt;
  &lt;h3&gt;表達式運用（串接）&lt;/h3&gt;
  &lt;p&gt;{{ imageSize }}&lt;/p&gt;
  &lt;input type="range" min="100" max="1000" v-model="imageSize" /&gt;
  &lt;br /&gt;
  &lt;img :src="`${breakfastShop.resizeImg}&amp;w=${imageSize}`" alt="" /&gt;
  &lt;br /&gt;
  {{ `${breakfastShop.resizeImg}&amp;w=${imageSize}` }}

  &lt;hr /&gt;
  &lt;h3&gt;動態屬性綁定(注意大小寫)&lt;/h3&gt;
  &lt;button
    type="button"
    v-on:click="dynamic = dynamic === 'disabled' ? 'readonly':'disabled'"
  &gt;
    切換為 {{ dynamic }}
  &lt;/button&gt;
  &lt;br /&gt;
  &lt;input type="text" :&#91;dynamic] :value="name" /&gt;

  &lt;hr /&gt;
  &lt;h3&gt;預告：元件的資料傳遞亦是使用 v-bind&lt;/h3&gt;
  &lt;ul&gt;
    &lt;list-item
      :item="item"
      v-for="(item, key) in products"
      :key="item.name + 2"
    &gt;&lt;/list-item&gt;
  &lt;/ul&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const App = {
  data() {
    return {
      name: "小明",
      isFull: false,
      link: "小明",
      imageSize: 200,
      dynamic: "disabled",
      breakfastShop: {
        name: "奇蹟早餐",
        imgUrl:
          "https://images.unsplash.com/photo-1600182610361-4b4d664e07b9?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=200&amp;q=80",
        resizeImg:
          "https://images.unsplash.com/photo-1600182610361-4b4d664e07b9?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;q=80",
      },
      products: &#91;
        {
          name: "蛋餅",
          price: 30,
          vegan: false,
          imgUrl:
            "https://images.unsplash.com/photo-1607278967323-8766a3a501e6?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=200&amp;q=80",
        },
        {
          name: "飯糰",
          price: 35,
          vegan: false,
          imgUrl:
            "https://images.unsplash.com/photo-1603245460565-5a7b42a6a6f4?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=200&amp;q=80",
        },
        {
          name: "小籠包",
          price: 60,
          vegan: false,
          imgUrl:
            "https://images.unsplash.com/photo-1595424265370-3e02d3e6c10c?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=200&amp;q=80",
        },
      ],
      productsObj: {
        chineseOmelette: {
          name: "蛋餅",
          price: 30,
          vegan: false,
        },
        riceBall: {
          name: "飯糰",
          price: 35,
          vegan: false,
        },
        soupDumpling: {
          name: "小籠包",
          price: 60,
          vegan: false,
        },
        radishCake: {
          name: "蘿蔔糕",
          price: 30,
          vegan: true,
        },
      },
    };
  },
  methods: {
    change: function (key) {
      this&#91;key] = !this&#91;key];
    },
  },
};

Vue.createApp(App)
  .component("list-item", {
    template: `
    &lt;li&gt;
      {{ item.name}} / {{ item.price }} 元
    &lt;/li&gt;
  `,
    props: &#91;"item"],
  })
  .mount("#app");
</code></pre>



<pre class="wp-block-code"><code>// CSS
.square-img {
  width: 100px;
  height: 100px;
  object-fit: cover;
}
</code></pre>



<h3 class="wp-block-heading">HTML 樣式綁定</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h2&gt;切換 Class&lt;/h2&gt;
  &lt;h3&gt;物件寫法&lt;/h3&gt;
  &lt;!-- key 值是對應 className，物件的值則是判斷式 --&gt;
  &lt;div class="box" :class="{ rotate: isTransform, 'bg-danger': boxColor }"&gt;&lt;/div&gt;
  &lt;hr&gt;
  &lt;button class="btn btn-outline-primary" v-on:click="change('isTransform')"&gt;選轉物件&lt;/button&gt;
  &lt;button class="btn btn-outline-primary ms-1" v-on:click="change('boxColor')"&gt;切換色彩&lt;/button&gt;

  &lt;hr class="mt-4"&gt;
  &lt;h3&gt;物件寫法 2&lt;/h5&gt;
  &lt;div class="box" :class="objectClass"&gt;&lt;/div&gt;

  &lt;hr&gt;
  &lt;h4&gt;陣列寫法&lt;/h4&gt;
  &lt;button class="btn" :class="&#91;'btn-primary', 'disabled']"&gt;請操作本元件&lt;/button&gt;
  &lt;button class="btn" :class="arrayClass"&gt;請操作本元件&lt;/button&gt;
  &lt;button type="button"
    class="btn btn-outline-primary"
    v-on:click="addClass(&#91;'btn-primary', 'disabled'])"&gt;為陣列加入 Class&lt;/button&gt;

  &lt;hr&gt;
  &lt;h2&gt;行內樣式&lt;/h2&gt;
  &lt;h4&gt;綁定行內樣式&lt;/h4&gt;
  &lt;!-- key 會帶入 style 的屬性 --&gt;
  &lt;!-- 值則是帶入 Style 相對應的值 --&gt;
  &lt;div class="box" :style="{ backgroundColor: 'red' }"&gt;&lt;/div&gt;
  &lt;div class="box" :style="styleObject"&gt;&lt;/div&gt;
  &lt;div class="box" :style="&#91;styleObject, styleObject2]"&gt;&lt;/div&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const App = {
  data() {
    return {
      isTransform: true,
      boxColor: false,
      objectClass: {
        rotate: true,
        'bg-danger': true
      },

      // Array 操作
      arrayClass: &#91;''],

      // 行內樣式
      // 使用駝峰式命名
      styleObject: {
        backgroundColor: 'red',
        borderWidth: '5px'
      },
      styleObject2: {
        boxShadow: '3px 3px 5px rgba(0, 0, 0, 0.16)'
      },
      styleObject3: {
        userSelect: 'none'
      }
    };
  },
  methods: {
    change: function (key) {
      this&#91;key] = !this&#91;key];
    },
    addClass(arr) {
      this.arrayClass.push(...arr);
    }
  },
};

Vue.createApp(App).mount('#app');
</code></pre>



<pre class="wp-block-code"><code>// CSS
.box {
  background-color: var(--bs-light);
  border: 1px solid var(--bs-gray);
  width: 80px;
  height: 80px;
}
.box {
  transition: all .5s;
}
.box.rotate {
  transform: rotate(45deg)
}
</code></pre>



<h3 class="wp-block-heading">資料雙向綁定 v-model</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;input&lt;/h3&gt;
  &lt;input type="text" class="form-control" v-model="name" /&gt;
  {{ name }}

  &lt;hr /&gt;
  &lt;h3&gt;textarea&lt;/h3&gt;
  &lt;textarea cols="30" rows="3" class="form-control" v-model="text"&gt;&lt;/textarea&gt;
  {{ text }}

  &lt;hr /&gt;
  &lt;h3&gt;checkbox 單選框&lt;/h3&gt;
  &lt;p&gt;小明，你是吃飽沒？&lt;/p&gt;
  &lt;p&gt;{{ checkAnswer }}&lt;/p&gt;
  &lt;p&gt;{{ checkAnswer ? '吃飽了' : '還沒'}}&lt;/p&gt;
  &lt;div class="form-check"&gt;
    &lt;input
      type="checkbox"
      class="form-check-input"
      id="check1"
      v-model="checkAnswer"
    /&gt;
    &lt;label class="form-check-label" for="check1"&gt;小明回覆&lt;/label&gt;
  &lt;/div&gt;

  &lt;hr /&gt;
  &lt;h3&gt;checkbox 單選延伸&lt;/h3&gt;
  &lt;p&gt;小明，你是吃飽沒？&lt;/p&gt;
  &lt;p&gt;{{ checkAnswer2 }}&lt;/p&gt;
  &lt;div class="form-check"&gt;
    &lt;input
      type="checkbox"
      v-model="checkAnswer2"
      true-value="吃飽了"
      false-value="還沒"
      class="form-check-input"
      id="check2"
    /&gt;
    &lt;label class="form-check-label" for="check2"&gt;小明回覆&lt;/label&gt;
  &lt;/div&gt;

  &lt;hr /&gt;
  &lt;h3&gt;checkbox 複選框&lt;/h3&gt;
  &lt;p&gt;你還要吃什麼？&lt;/p&gt;
  &lt;p&gt;{{ checkAnswer3.join(' ') }}&lt;/p&gt;
  &lt;div class="form-check"&gt;
    &lt;input
      type="checkbox"
      class="form-check-input"
      id="check3"
      value="蛋餅"
      v-model="checkAnswer3"
    /&gt;
    &lt;label class="form-check-label" for="check3"&gt;蛋餅&lt;/label&gt;
  &lt;/div&gt;
  &lt;div class="form-check"&gt;
    &lt;input
      type="checkbox"
      class="form-check-input"
      id="check4"
      value="蘿蔔糕"
      v-model="checkAnswer3"
    /&gt;
    &lt;label class="form-check-label" for="check4"&gt;蘿蔔糕&lt;/label&gt;
  &lt;/div&gt;
  &lt;div class="form-check"&gt;
    &lt;input
      type="checkbox"
      class="form-check-input"
      id="check5"
      value="豆漿"
      v-model="checkAnswer3"
    /&gt;
    &lt;label class="form-check-label" for="check5"&gt;豆漿&lt;/label&gt;
  &lt;/div&gt;

  &lt;hr /&gt;
  &lt;h3&gt;radio 單選框&lt;/h3&gt;
  &lt;p&gt;你還要吃什麼？&lt;/p&gt;
  &lt;p&gt;{{ radioAnswer }}&lt;/p&gt;
  &lt;div class="form-check"&gt;
    &lt;input
      type="radio"
      v-model="radioAnswer"
      class="form-check-input"
      id="radio1"
      value="蛋餅"
    /&gt;
    &lt;label class="form-check-label" for="radio1"&gt;蛋餅&lt;/label&gt;
  &lt;/div&gt;
  &lt;div class="form-check"&gt;
    &lt;input
      type="radio"
      v-model="radioAnswer"
      class="form-check-input"
      id="radio2"
      value="蘿蔔糕"
    /&gt;
    &lt;label class="form-check-label" for="radio2"&gt;蘿蔔糕&lt;/label&gt;
  &lt;/div&gt;
  &lt;div class="form-check"&gt;
    &lt;input
      type="radio"
      v-model="radioAnswer"
      class="form-check-input"
      id="radio3"
      value="豆漿"
    /&gt;
    &lt;label class="form-check-label" for="radio3"&gt;豆漿&lt;/label&gt;
  &lt;/div&gt;

  &lt;hr /&gt;
  &lt;h3&gt;select 單選&lt;/h3&gt;
  &lt;p&gt;你還要吃什麼？&lt;/p&gt;
  &lt;p&gt;{{ selectAnswer }}&lt;/p&gt;
  &lt;select class="form-select" v-model="selectAnswer"&gt;
    &lt;option value="" disabled&gt;說吧，你要吃什麼？&lt;/option&gt;
    &lt;!-- &lt;option value="1234"&gt;12345&lt;/option&gt; --&gt;
    &lt;option :value="item.name" v-for="item in products" :key="item.name"&gt;
      {{ item.name }} / {{ item.price }} 元
    &lt;/option&gt;
  &lt;/select&gt;

  &lt;hr /&gt;
  &lt;h3&gt;select 多選&lt;/h3&gt;
  &lt;p&gt;你還要吃什麼？&lt;/p&gt;
  &lt;p&gt;{{ selectAnswer2 }}&lt;/p&gt;
  &lt;select class="form-select" multiple v-model="selectAnswer2"&gt;
    &lt;option selected disabled value=""&gt;說吧，你要吃什麼？&lt;/option&gt;
    &lt;option :value="item.name" v-for="item in products" :key="item.name"&gt;
      {{item.name}} / {{item.price}} 元
    &lt;/option&gt;
  &lt;/select&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const App = {
  data() {
    return {
      name: "小明",
      text: "一段文字敘述",
      checkAnswer: false,
      checkAnswer2: "",
      checkAnswer3: &#91;],
      radioAnswer: "蛋餅",
      selectAnswer: "",
      selectAnswer2: &#91;],
      products: &#91;
        {
          name: "蛋餅",
          price: 30,
          vegan: false,
        },
        {
          name: "飯糰",
          price: 35,
          vegan: false,
        },
        {
          name: "小籠包",
          price: 60,
          vegan: false,
        },
        {
          name: "蘿蔔糕",
          price: 30,
          vegan: true,
        },
      ],
      productsObj: {
        chineseOmelette: {
          name: "蛋餅",
          price: 30,
          vegan: false,
        },
        riceBall: {
          name: "飯糰",
          price: 35,
          vegan: false,
        },
        soupDumpling: {
          name: "小籠包",
          price: 60,
          vegan: false,
        },
        radishCake: {
          name: "蘿蔔糕",
          price: 30,
          vegan: true,
        },
      },
    };
  },
  methods: {
    reverseArray: function () {
      this.products.reverse();
    },
  },
};

Vue.createApp(App)
  .component("list-item", {
    template: `
    &lt;li&gt;
      {{ item.name}} / {{ item.price }} 元
    &lt;/li&gt;
  `,
    props: &#91;"item"],
  })
  .mount("#app");
</code></pre>



<h3 class="wp-block-heading">v-model 修飾符</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;修飾符&lt;/h3&gt;
  &lt;h4 class="mt-3"&gt;延遲 Lazy&lt;/h4&gt;
  {{ lazyMsg }}
  &lt;input type="text" class="form-control" v-model.lazy="lazyMsg" /&gt;
  &lt;h4 class="mt-3"&gt;純數值 Number&lt;/h4&gt;
  {{ numberMsg }}{{ typeof numberMsg }}
  &lt;input type="number" class="form-control" v-model.number="numberMsg" /&gt;
  &lt;h4 class="mt-3"&gt;修剪 Trim&lt;/h4&gt;
  這是一段{{ trimMsg }}緊黏的文字
  &lt;input type="text" class="form-control" v-model.trim="trimMsg" /&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const App = {
  data() {
    return {
      lazyMsg: "",
      numberMsg: "",
      trimMsg: "",
    };
  },
};

Vue.createApp(App).mount("#app");
</code></pre>



<h3 class="wp-block-heading">事件觸發 v-on</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;觸發事件 與 縮寫*&lt;/h3&gt;
  &lt;div class="box" :class="{ rotate: isTransform }"&gt;&lt;/div&gt;
  &lt;hr /&gt;
  &lt;!-- v-on:click 縮寫寫法 @click --&gt;
  &lt;button class="btn btn-outline-primary" @click="changeClass"&gt;選轉物件&lt;/button&gt;
  &lt;br /&gt;
  &lt;h3&gt;帶入參數*&lt;/h3&gt;
  &lt;div class="box" :class="{ rotate: isTransform }"&gt;&lt;/div&gt;
  &lt;hr /&gt;
  &lt;button class="btn btn-outline-primary" v-on:click="change('isTransform')"&gt;
    選轉物件
  &lt;/button&gt;

  &lt;hr /&gt;
  &lt;h3&gt;原生 DOM 事件*&lt;/h3&gt;
  &lt;!-- https://developer.mozilla.org/en-US/docs/Web/Events --&gt;
  &lt;h4&gt;input change 事件&lt;/h4&gt;
  &lt;input type="text" @change="onChange" /&gt;
  &lt;br /&gt;&lt;br /&gt;
  &lt;h4&gt;form submit 事件&lt;/h4&gt;
  &lt;form @submit.prevent="submitForm"&gt;
    &lt;input type="text" v-model="name" /&gt;
    &lt;button&gt;送出表單&lt;/button&gt;
  &lt;/form&gt;
  &lt;hr /&gt;
  &lt;h3&gt;動態事件 &#91;]&lt;/h3&gt;
  &lt;input type="text" @&#91;event]="dynamicEvent" /&gt;
  &lt;input type="text" v-model="event" /&gt;

  &lt;hr /&gt;
  &lt;h3&gt;動態物件方法 {}&lt;/h3&gt;
  &lt;!-- 此方法無法傳入參數 --&gt;
  &lt;button class="box" @="{ mousedown: down, mouseup: up }"&gt;&lt;/button&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const App = {
  data() {
    return {
      name: "小明",
      isTransform: true,
      num: 10,
      event: "click",
    };
  },
  methods: {
    changeClass() {
      this.isTransform = !this.isTransform;
    },
    change(key) {
      this&#91;key] = !this&#91;key];
    },
    onChange(evt) {
      console.log("change 事件");
      console.log(evt);
    },
    submitForm() {
      console.log("表單已送出", `name 為 ${this.name}`);
    },
    dynamicEvent() {
      console.log("這是一個動態事件", this.event);
    },
    down() {
      console.log("按下");
    },
    up() {
      console.log("放開");
    },
  },
};

Vue.createApp(App).mount("#app");
</code></pre>



<pre class="wp-block-code"><code>// CSS
.box {
  background-color: var(--bs-light);
  border: 1px solid var(--bs-gray);
  width: 80px;
  height: 80px;
}
.box {
  transition: all 0.5s;
}
.box.rotate {
  transform: rotate(45deg);
}
</code></pre>



<h3 class="wp-block-heading">v-on 修飾符</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;修飾符&lt;/h3&gt;
  &lt;h4&gt;按鍵修飾符*&lt;/h4&gt;
  &lt;ul&gt;
    &lt;li&gt;keyAlias - 只當事件是從特定鍵觸發時才觸發。&lt;/li&gt;
    &lt;li&gt;
      別名修飾 - .enter, .tab, .delete, .esc, .space, .up, .down, .left, .right
    &lt;/li&gt;
    &lt;li&gt;
      修飾符來實現僅在按下相應按鍵時才觸發鼠標或鍵盤事件的監聽器 - .ctrl, .alt,
      .shift, .meta
    &lt;/li&gt;
  &lt;/ul&gt;

  &lt;h6 class="mt-3"&gt;別名修飾&lt;/h6&gt;
  &lt;input
    type="text"
    class="form-control"
    v-model="text"
    @keyup.enter="trigger('enter')"
  /&gt;

  &lt;h6 class="mt-3"&gt;相應按鍵時才觸發的監聽器&lt;/h6&gt;
  &lt;input
    type="text"
    class="form-control"
    v-model="text"
    @keyup.shift.enter="trigger('shift + Enter')"
  /&gt;

  &lt;h6 class="mt-3"&gt;特定鍵&lt;/h6&gt;
  &lt;input
    type="text"
    class="form-control"
    v-model="text"
    @keyup.h="trigger('h')"
  /&gt;
  &lt;hr /&gt;
  &lt;h4&gt;滑鼠修飾符&lt;/h4&gt;
  &lt;ul&gt;
    &lt;li&gt;.left 只當點擊鼠標左鍵時觸發。&lt;/li&gt;
    &lt;li&gt;.right 只當點擊鼠標右鍵時觸發。&lt;/li&gt;
    &lt;li&gt;.middle 只當點擊鼠標中鍵時觸發。&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h6 class="mt-3"&gt;滑鼠修飾符&lt;/h6&gt;
  &lt;div class="p-3 bg-primary"&gt;
    &lt;span class="box" @click.right="trigger('right button')"&gt; &lt;/span&gt;
  &lt;/div&gt;

  &lt;hr /&gt;
  &lt;h4&gt;事件修飾符&lt;/h4&gt;
  &lt;ul&gt;
    &lt;li&gt;.stop - 調用 event.stopPropagation()。&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;.prevent - 調用 event.preventDefault()。&lt;/strong&gt;&lt;/li&gt;
    &lt;li&gt;.capture - 添加事件偵聽器時使用 capture 模式。&lt;/li&gt;
    &lt;li&gt;.self - 只當事件是從偵聽器綁定的元素本身觸發時才觸發回調。&lt;/li&gt;
    &lt;li&gt;.once - 只觸發一次回調。&lt;/li&gt;
  &lt;/ul&gt;
  &lt;a href="https://www.google.com/" @click.prevent="trigger('prevent')"
    &gt;加入 Prevent&lt;/a
  &gt;

  &lt;hr /&gt;
  &lt;h6&gt;預設情境&lt;/h6&gt;
  &lt;a href="https://javascript.info/bubbling-and-capturing"&gt;冒泡事件參考文章&lt;/a&gt;
  &lt;div class="p-3 bg-primary" @click="trigger('div')"&gt;
    &lt;span
      class="box d-flex align-items-center justify-content-center"
      @click="trigger('box')"
    &gt;
      &lt;button class="btn btn-outline-secondary" @click="trigger('button')"&gt;
        按我
      &lt;/button&gt;
    &lt;/span&gt;
  &lt;/div&gt;

  &lt;h6 class="mt-3"&gt;stopPropagation (防止向外尋找)&lt;/h6&gt;
  &lt;div class="p-3 bg-primary" @click="trigger('div')"&gt;
    &lt;span
      class="box d-flex align-items-center justify-content-center"
      @click.stop="trigger('box')"
    &gt;
      &lt;button class="btn btn-outline-secondary" @click="trigger('button')"&gt;
        按我
      &lt;/button&gt;
    &lt;/span&gt;
  &lt;/div&gt;

  &lt;h6 class="mt-3"&gt;事件偵聽器時使用 capture 模式 (事件改為由外而內)&lt;/h6&gt;
  &lt;div class="p-3 bg-primary" @click.capture="trigger('div')"&gt;
    &lt;span
      class="box d-flex align-items-center justify-content-center"
      @click.capture="trigger('box')"
    &gt;
      &lt;button
        class="btn btn-outline-secondary"
        @click.capture="trigger('button')"
      &gt;
        按我
      &lt;/button&gt;
    &lt;/span&gt;
  &lt;/div&gt;

  &lt;h6 class="mt-3"&gt;事件偵聽器時使用 self 模式 (只會觸發自己範圍內的)&lt;/h6&gt;
  &lt;div class="p-3 bg-primary" @click.self="trigger('div')"&gt;
    &lt;span
      class="box d-flex align-items-center justify-content-center"
      @click.self="trigger('box')"
    &gt;
      &lt;button class="btn btn-outline-secondary" @click.self="trigger('button')"&gt;
        按我
      &lt;/button&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;hr /&gt;
  &lt;h3 class="mt-3"&gt;事件偵聽器只觸發一次 once&lt;/h3&gt;
  &lt;div class="p-3 bg-primary" @click.once="trigger('div')"&gt;
    &lt;span
      class="box d-flex align-items-center justify-content-center"
      @click.once="trigger('box')"
    &gt;
      &lt;button class="btn btn-outline-secondary" @click.once="trigger('button')"&gt;
        按我
      &lt;/button&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const App = {
  data() {
    return {
      text: "小明",
      isTransform: true,
      num: 10,
      event: "click",
    };
  },
  methods: {
    trigger: function (name) {
      console.log(name, "此事件被觸發了");
    },
  },
};
Vue.createApp(App).mount("#app");
</code></pre>



<pre class="wp-block-code"><code>// CSS
.box {
  display: block;
  background-color: var(--bs-light);
  border: 1px solid var(--bs-gray);
  width: 80px;
  height: 80px;
}
.box {
  transition: all 0.5s;
}
.box.rotate {
  transform: rotate(45deg);
}
</code></pre>



<h3 class="wp-block-heading">v-on DOM 事件處理技巧</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;h3&gt;原生 DOM 事件&lt;/h3&gt;
  &lt;a href="https://www.w3schools.com/jsref/dom_obj_event.asp"&gt;參考：DOM 事件&lt;/a&gt;
  &lt;div class="box" :class="{ rotate: isTransform }"&gt;&lt;/div&gt;
  &lt;hr /&gt;
  &lt;!-- 當沒有參數時，預設第一個則是 dom 事件參數 --&gt;
  &lt;button class="btn btn-outline-primary" @click="changeClass"&gt;選轉物件&lt;/button&gt;
  &lt;button class="btn btn-outline-primary" @keyup.enter="changeClass"&gt;
    按鈕事件
  &lt;/button&gt;
  &lt;!-- 當如果有參數時，則可以使用 $event --&gt;
  &lt;button
    class="btn btn-outline-primary"
    @click="changeClassWithEvent('這段是自訂參數', $event)"
  &gt;
    自訂參數
  &lt;/button&gt;
  &lt;br /&gt;
  &lt;br /&gt;
  &lt;h3&gt;取得原生 input 數值&lt;/h3&gt;
  &lt;input type="text" @change="inputEvent" /&gt;
  &lt;br /&gt;&lt;br /&gt;
  &lt;h3&gt;監聽按鍵事件&lt;/h3&gt;
  &lt;input type="text" @keyup="keyboardEvent" /&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const App = {
  data() {
    return {
      text: "小明",
      isTransform: true,
      num: 10,
    };
  },
  methods: {
    changeClass: function (e) {
      console.log("dom", e);
      this.isTransform = !this.isTransform;
    },
    changeClassWithEvent(parameter, e) {
      console.log(parameter, e);
    },
    inputEvent(e) {
      console.log(e.target.value);
    },
    keyboardEvent(e) {
      console.dir(e.keyCode);
      if (e.keyCode === 13) {
        alert("你按下了 Enter");
      } else if (e.keyCode === 32) {
        alert("你按下了 空白鍵");
      }
    },
  },
};
Vue.createApp(App).mount("#app");
</code></pre>



<pre class="wp-block-code"><code>// CSS
.box {
  display: block;
  background-color: var(--bs-light);
  border: 1px solid var(--bs-gray);
  width: 80px;
  height: 80px;
}
.box {
  transition: all 0.5s;
}
.box.rotate {
  transform: rotate(45deg);
}
</code></pre>



<h3 class="wp-block-heading">指令章節作業 – 簡單版</h3>



<ol class="wp-block-list">
<li>試著拆解流程
<ul class="wp-block-list">
<li>左邊的清單完成</li>



<li>選擇品項</li>



<li>訂單列表</li>



<li>調整品項細節</li>
</ul>
</li>



<li>先觀察解答</li>



<li>第二次製作，就完全不要看解答</li>
</ol>



<h3 class="wp-block-heading">指令章節作業 – 簡單版實作示範</h3>



<ol class="wp-block-list">
<li>先準備左邊的品項呈現</li>



<li>準備暫存資料</li>



<li>暫存資料轉為訂單內容</li>



<li>計算總金額</li>



<li>客製化選項</li>
</ol>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;div class="container gx-2"&gt;
    &lt;div class="row gx-3 bg-light py-3"&gt;
      &lt;!-- 1. 先準備左邊的品項呈現 --&gt;
      &lt;!-- 2-2. 觸發點擊事件 --&gt;
      &lt;!-- 2-3. v-bind:class 樣式綁定 --&gt;
      &lt;!-- key 值是對應 className，物件的值則是判斷式 --&gt;
      &lt;div class="col-md-4"&gt;
        &lt;div class="list-group"&gt;
          &lt;a
            href="#"
            class="list-group-item list-group-item-action"
            v-for="item in products"
            :key="item.name"
            @click.prevent="selectProduct(item)"
            :class="{ 'active': itemSelected.name === item.name }"
          &gt;
            &lt;h6 class="card-title mb-1"&gt;{{ item.name }}&lt;/h6&gt;
            &lt;div class="d-flex align-items-center justify-content-between"&gt;
              &lt;p class="mb-0"&gt;&lt;small&gt;{{ item.engName }}&lt;/small&gt;&lt;/p&gt;
              &lt;p class="mb-0"&gt;&lt;small&gt;NT$ {{ item.price }}&lt;/small&gt;&lt;/p&gt;
            &lt;/div&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="col-md-8"&gt;
        &lt;div class="card mb-2"&gt;
          &lt;!-- 2-4. 條件判斷 v-if --&gt;
          &lt;div
            v-if="!itemSelected.name"
            class="position-absolute text-white d-flex align-items-center justify-content-center active"
            style="
              top: 0;
              bottom: 0;
              left: 0;
              right: 0;
              background-color: rgba(0, 0, 0, 0.65);
              z-index: 100;
            "
          &gt;
            請先選擇飲品
          &lt;/div&gt;
          &lt;!-- 5. 客製化選項 --&gt;
          &lt;!-- 5-1. 資料雙向綁定 v-model  --&gt;
          &lt;div class="card-body px-4"&gt;
            &lt;div class="mb-3"&gt;
              &lt;label for="productNum" class="form-label"&gt;數量&lt;/label&gt;
              &lt;input
                type="number"
                class="form-control"
                id="productNum"
                v-model="itemSelected.count"
                placeholder="數量"
                min="0"
              /&gt;
            &lt;/div&gt;
            &lt;div class="mb-3"&gt;
              &lt;label for="productIce" class="form-label d-block"&gt;冰塊*&lt;/label&gt;
              &lt;div
                class="form-check form-check-inline"
                v-for="(ice, key) in iceType"
                :key="'ice' + key"
              &gt;
                &lt;input
                  class="form-check-input"
                  name="iceType"
                  type="radio"
                  :id="'ice' + key"
                  :value="ice"
                  v-model="itemSelected.ice"
                /&gt;
                &lt;label class="form-check-label" :for="'ice' + key"
                  &gt;{{ ice }}&lt;/label
                &gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;div class="mb-3"&gt;
              &lt;label for="productSugar" class="form-label d-block"&gt;甜度*&lt;/label&gt;
              &lt;div
                class="form-check form-check-inline"
                v-for="(sugar, key) in sugarType"
                :key="'sugar' + key"
              &gt;
                &lt;input
                  class="form-check-input"
                  name="sugarType"
                  type="radio"
                  :id="'sugar' + key"
                  :value="sugar"
                  v-model="itemSelected.sugar"
                /&gt;
                &lt;label class="form-check-label" :for="'sugar' + key"
                  &gt;{{ sugar }}&lt;/label
                &gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;!-- 5-6. 製作加料是相對複雜的部份 --&gt;
            &lt;!-- 5-7. v-for --&gt;
            &lt;div class="mb-3"&gt;
              &lt;label for="productTopping" class="form-label d-block"
                &gt;加料&lt;/label
              &gt;
              &lt;div
                class="form-check form-check-inline"
                v-for="(topping, key) in toppingsType"
                :key="'topping' + key"
              &gt;
                &lt;!-- 5-9. 把加料的值取出來 --&gt;
                &lt;!-- 5-10. 資料雙向綁定 v-model --&gt;
                &lt;input
                  class="form-check-input"
                  type="checkbox"
                  :id="'topping' + key"
                  :value="topping"
                  v-model="itemSelected.toppings"
                /&gt;
                &lt;label class="form-check-label" :for="'topping' + key"
                  &gt;{{ topping }}&lt;/label
                &gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;div class="mb-3"&gt;
              &lt;label for="productNotice" class="form-label"&gt;備註&lt;/label&gt;
              &lt;textarea
                class="form-control"
                id="productNotice"
                rows="2"
                v-model="itemSelected.notice"
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;
            &lt;div class="d-flex gap-2"&gt;
              &lt;!-- 5-5. 觸發取消清空 --&gt;
              &lt;button
                class="btn btn-outline-primary w-100"
                @click.prevent="resetOrder"
                type="button"
              &gt;
                取消
              &lt;/button&gt;
              &lt;!-- 3-2. 觸發加到購物車 --&gt;
              &lt;button
                class="btn btn-primary w-100"
                @click.prevent="addToOrder(itemSelected)"
                type="button"
                :disabled="!itemSelected.name"
              &gt;
                加入
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="card" v-if="orderList.length &gt; 0"&gt;
          &lt;div class="card-body"&gt;
            &lt;table class="table"&gt;
              &lt;thead&gt;
                &lt;tr&gt;
                  &lt;th scope="col"&gt;品項&lt;/th&gt;
                  &lt;th scope="col"&gt;冰塊&lt;/th&gt;
                  &lt;th scope="col"&gt;甜度&lt;/th&gt;
                  &lt;th scope="col"&gt;加料&lt;/th&gt;
                  &lt;th scope="col"&gt;單價&lt;/th&gt;
                  &lt;th scope="col"&gt;數量&lt;/th&gt;
                  &lt;th scope="col"&gt;小計&lt;/th&gt;
                &lt;/tr&gt;
              &lt;/thead&gt;
              &lt;tbody&gt;
                &lt;!-- 3-4. 呈現在列表畫面上 --&gt;
                &lt;!-- 5-11. 把加料呈現在列表畫面上，並轉成字串 --&gt;
                &lt;!-- 5-13. 調整加上加料後的單價 --&gt;
                &lt;tr v-for="(item, key) in orderList" :key="'order' + key"&gt;
                  &lt;th scope="row"&gt;
                    {{ item.name }}&lt;br /&gt;
                    &lt;small
                      class="text-muted fw-normal"
                      v-if="item.notice !== ''"
                      &gt;備註：{{ item.notice }}&lt;/small
                    &gt;
                  &lt;/th&gt;
                  &lt;td&gt;{{ item.ice }}&lt;/td&gt;
                  &lt;td&gt;{{ item.sugar }}&lt;/td&gt;
                  &lt;td&gt;{{ item.toppings.toString() }}&lt;/td&gt;
                  &lt;td&gt;{{ item.price + item.toppings.length * 10 }}&lt;/td&gt;
                  &lt;td&gt;{{ item.count }}&lt;/td&gt;
                  &lt;td class="text-end"&gt;{{ item.total }}&lt;/td&gt;
                &lt;/tr&gt;
              &lt;/tbody&gt;
            &lt;/table&gt;
            &lt;p class="text-end"&gt;共 NT$ {{ orderTotal }} 元&lt;/p&gt;
            &lt;button
              class="btn btn-sm btn-secondary w-100"
              :disabled="orderList.length === 0"
              @click="resetOrder"
            &gt;
              重新選擇
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const App = {
  data() {
    return {
      // 2. 準備暫存資料
      itemSelected: {},
      // 3-3. 購物車列表
      orderList: &#91;],
      // 4-3. 訂單總價
      orderTotal: 0,
      iceType: &#91;"正常冰", "少冰", "微冰", "去冰", "熱"],
      sugarType: &#91;"全糖", "七分", "半糖", "三分", "無糖"],
      toppingsType: &#91;"珍珠", "粉條", "小粉圓", "椰果", "芋頭"],
      products: &#91;
        {
          name: "珍珠鮮奶茶",
          engName: "Pearl Milk Tea",
          price: 60,
        },
        {
          name: "鮮奶茶",
          engName: "Milk Tea",
          price: 50,
        },
        {
          name: "古意冬瓜茶",
          engName: "Winter Melon Drink",
          price: 30,
        },
        {
          name: "蜜香紅茶",
          engName: "Black Tea",
          price: 30,
        },
        {
          name: "包種青茶",
          engName: "Pouchong tea",
          price: 35,
        },
        {
          name: "檸檬烏龍",
          engName: "Lemon Oolong Tea",
          price: 55,
        },
        {
          name: "薑母茶",
          engName: "Ginger Tea",
          price: 55,
        },
        {
          name: "青草茶",
          engName: "Herbal Tea",
          price: 35,
        },
        {
          name: "金桔檸檬",
          engName: "Kumquat Lemonade",
          price: 40,
        },
        {
          name: "柳澄青茶",
          engName: "Orange Mountain Tea",
          price: 45,
        },
      ],
    };
  },
  methods: {
    // 2-1. 選擇品項
    selectProduct(product) {
      this.itemSelected = {
        // 透過解構方式避免影響到原始資料
        ...product,
        // 3-5. 加上預設值
        count: 1,
        // 5-8. 加入加料項目
        toppings: &#91;],
        ice: "正常冰",
        sugar: "全糖",
        notice: "",
      };
    },
    // 3-1. 加到購物車
    addToOrder(product) {
      // 4. 計算總金額
      // 4-1. 重新整理訂單所需要的內容
      const order = {
        // 透過解構方式
        ...product,
        // 小計
        // 5-12. 加料
        total: (product.price + product.toppings.length * 10) * product.count,
      };
      // 4-2. product → order
      this.orderList.push(order);
      // 4-5. 計算當前的總價
      this.countTotal();
      // 5-4. 使用 resetOrder 方法加入後清空
      this.resetOrder();
    },
    // 4-4. 計算總價
    countTotal() {
      // 4-6. 每次計算先清空總價
      this.orderTotal = 0;
      this.orderList.forEach((item) =&gt; {
        this.orderTotal += item.total;
      });
    },
    // 5-3. 清空方法
    resetOrder() {
      // 5-2. 加入後清空
      this.itemSelected = {};
    },
  },
};

Vue.createApp(App).mount("#app");
</code></pre>



<h3 class="wp-block-heading">指令章節作業</h3>



<ul class="wp-block-list">
<li>先寫出流程，一個一個拆解
<ol class="wp-block-list">
<li>列表</li>



<li>表單</li>



<li>加入購物車</li>
</ol>
</li>



<li>請同時撰寫筆記，製作第二次，僅參考自己筆記完成</li>
</ul>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;div class="container py-5 gx-2"&gt;
    &lt;h2 class="text-center"&gt;指令章節作業&lt;/h2&gt;
    &lt;div class="row gx-3 bg-light py-3"&gt;
      &lt;div class="col-md-4"&gt;
        &lt;div class="list-group"&gt;
          &lt;a
            href="#"
            class="list-group-item list-group-item-action"
            v-for="item in products"
            :key="item.name"
            @click.prevent="selectProduct(item)"
            :class="{ 'active': itemSelected.name === item.name }"
          &gt;
            &lt;h6 class="card-title mb-1"&gt;{{ item.name }}&lt;/h6&gt;
            &lt;div class="d-flex align-items-center justify-content-between"&gt;
              &lt;p class="mb-0"&gt;&lt;small&gt;{{ item.engName }}&lt;/small&gt;&lt;/p&gt;
              &lt;p class="mb-0"&gt;&lt;small&gt;NT$ {{ item.price }}&lt;/small&gt;&lt;/p&gt;
            &lt;/div&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="col-md-8"&gt;
        &lt;div class="card mb-2"&gt;
          &lt;!-- 條件判斷 v-if --&gt;
          &lt;div
            v-if="!itemSelected.hasOwnProperty('name')"
            class="position-absolute text-white d-flex align-items-center justify-content-center"
            style="
              top: 0;
              bottom: 0;
              left: 0;
              right: 0;
              background-color: rgba(0, 0, 0, 0.65);
              z-index: 100;
            "
          &gt;
            請先選擇飲品
          &lt;/div&gt;
          &lt;div class="card-body px-4"&gt;
            &lt;div class="mb-3"&gt;
              &lt;label for="productNum" class="form-label"&gt;數量&lt;/label&gt;
              &lt;input
                type="number"
                class="form-control"
                id="productNum"
                v-model="itemSelected.count"
                placeholder="數量"
                min="0"
              /&gt;
            &lt;/div&gt;
            &lt;div class="mb-3"&gt;
              &lt;label for="productIce" class="form-label d-block"&gt;冰塊*&lt;/label&gt;
              &lt;div
                class="form-check form-check-inline"
                v-for="(ice, key) in iceType"
                :key="'ice' + key"
              &gt;
                &lt;!-- HTML 屬性綁定 :disabled --&gt;
                &lt;input
                  class="form-check-input"
                  name="iceType"
                  type="radio"
                  :id="'ice' + key"
                  :value="ice"
                  v-model="itemSelected.ice"
                  :disabled="!itemSelected.hasOwnProperty('defaults') || (itemSelected.defaults.ice !== '' &amp;&amp; itemSelected.defaults.ice !== ice)"
                /&gt;
                &lt;label class="form-check-label" :for="'ice' + key"
                  &gt;{{ ice }}&lt;/label
                &gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;div class="mb-3"&gt;
              &lt;label for="productSugar" class="form-label d-block"&gt;甜度*&lt;/label&gt;
              &lt;div
                class="form-check form-check-inline"
                v-for="(sugar, key) in sugarType"
                :key="'sugar' + key"
              &gt;
                &lt;!-- HTML 屬性綁定 :disabled --&gt;
                &lt;input
                  class="form-check-input"
                  name="sugarType"
                  type="radio"
                  :id="'sugar' + key"
                  :value="sugar"
                  v-model="itemSelected.sugar"
                  :disabled="!itemSelected.hasOwnProperty('defaults') || (itemSelected.defaults.sugar !== '' &amp;&amp; itemSelected.defaults.sugar !== sugar)"
                /&gt;
                &lt;label class="form-check-label" :for="'sugar' + key"
                  &gt;{{ sugar }}&lt;/label
                &gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;div class="mb-3"&gt;
              &lt;label for="productTopping" class="form-label d-block"
                &gt;加料&lt;/label
              &gt;
              &lt;div
                class="form-check form-check-inline"
                v-for="(topping, key) in toppingsType"
                :key="'topping' + key"
              &gt;
                &lt;!-- HTML 屬性綁定 :disabled --&gt;
                &lt;input
                  class="form-check-input"
                  type="checkbox"
                  :id="'topping' + key"
                  :value="topping"
                  v-model="itemSelected.toppings"
                  :disabled="!itemSelected.hasOwnProperty('defaults') || (itemSelected.defaults.toppings.includes(topping))"
                /&gt;
                &lt;label class="form-check-label" :for="'topping' + key"
                  &gt;{{ topping }}&lt;/label
                &gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;div class="mb-3"&gt;
              &lt;label for="productNotice" class="form-label"&gt;備註&lt;/label&gt;
              &lt;textarea
                class="form-control"
                id="productNotice"
                rows="2"
                v-model="itemSelected.notice"
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;
            &lt;div class="d-flex gap-2"&gt;
              &lt;button
                class="btn btn-outline-primary w-100"
                @click.prevent="resetOrder"
                type="button"
              &gt;
                取消
              &lt;/button&gt;
              &lt;!-- HTML 屬性綁定 :disabled --&gt;
              &lt;button
                class="btn btn-primary w-100"
                @click.prevent="addToOrder(itemSelected)"
                :disabled="!itemSelected.hasOwnProperty('name')"
                type="button"
              &gt;
                加入
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;!-- 條件判斷 v-if --&gt;
        &lt;div class="card" v-if="orderList.length &gt; 0"&gt;
          &lt;div class="card-body"&gt;
            &lt;table class="table"&gt;
              &lt;thead&gt;
                &lt;tr&gt;
                  &lt;th scope="col"&gt;品項&lt;/th&gt;
                  &lt;th scope="col"&gt;冰塊&lt;/th&gt;
                  &lt;th scope="col"&gt;甜度&lt;/th&gt;
                  &lt;th scope="col"&gt;加料&lt;/th&gt;
                  &lt;th scope="col"&gt;單價&lt;/th&gt;
                  &lt;th scope="col"&gt;數量&lt;/th&gt;
                  &lt;th scope="col"&gt;小計&lt;/th&gt;
                &lt;/tr&gt;
              &lt;/thead&gt;
              &lt;tbody&gt;
                &lt;!-- 多筆資料渲染 v-for --&gt;
                &lt;tr v-for="(item, key) in orderList" :key="'order' + key"&gt;
                  &lt;th scope="row"&gt;
                    {{ item.name }}&lt;br /&gt;
                    &lt;!-- 條件判斷 v-if --&gt;
                    &lt;small
                      v-if="item.notice !== ''"
                      class="text-muted fw-normal"
                      &gt;備註：{{ item.notice }}&lt;/small
                    &gt;
                  &lt;/th&gt;
                  &lt;td&gt;{{ item.ice }}&lt;/td&gt;
                  &lt;td&gt;{{ item.sugar }}&lt;/td&gt;
                  &lt;td&gt;{{ item.toppings.toString() }}&lt;/td&gt;
                  &lt;td&gt;{{ item.price + item.toppings.length * 10 }}&lt;/td&gt;
                  &lt;td&gt;{{ item.count }}&lt;/td&gt;
                  &lt;td class="text-end"&gt;{{ item.total }}&lt;/td&gt;
                &lt;/tr&gt;
              &lt;/tbody&gt;
            &lt;/table&gt;
            &lt;p class="text-end"&gt;共 NT$ {{ orderTotal }} 元&lt;/p&gt;
            &lt;!-- HTML 屬性綁定 :disabled --&gt;
            &lt;!-- 事件觸發 v-on generateOrder() --&gt;
            &lt;button
              class="btn btn-sm btn-primary w-100"
              :disabled="orderList.length === 0"
              @click.prevent="generateOrder(orderList, orderTotal)"
            &gt;
              產生訂單
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;!-- 產生訂單 --&gt;
  &lt;!-- 條件判斷 v-if --&gt;
  &lt;div class="bg-light p-3 mt-3" v-if="checkedOrder.orders.length &gt; 0"&gt;
    &lt;div class="bg-white p-3 d-flex flex-column" style="min-height: 450px"&gt;
      &lt;table class="table"&gt;
        &lt;thead&gt;
          &lt;tr&gt;
            &lt;th scope="col"&gt;品項&lt;/th&gt;
            &lt;th scope="col"&gt;冰塊&lt;/th&gt;
            &lt;th scope="col"&gt;甜度&lt;/th&gt;
            &lt;th scope="col"&gt;加料&lt;/th&gt;
            &lt;th scope="col"&gt;單價&lt;/th&gt;
            &lt;th scope="col"&gt;數量&lt;/th&gt;
            &lt;th scope="col"&gt;小計&lt;/th&gt;
          &lt;/tr&gt;
        &lt;/thead&gt;
        &lt;tbody&gt;
          &lt;!-- 多筆資料渲染 v-for --&gt;
          &lt;tr v-for="(item, key) in checkedOrder.orders" :key="'order' + key"&gt;
            &lt;th scope="row"&gt;
              {{ item.name }}&lt;br /&gt;
              &lt;!-- 條件判斷 v-if --&gt;
              &lt;small v-if="item.notice !== ''" class="text-muted fw-normal"
                &gt;備註：{{ item.notice }}&lt;/small
              &gt;
            &lt;/th&gt;
            &lt;td&gt;{{ item.ice }}&lt;/td&gt;
            &lt;td&gt;{{ item.sugar }}&lt;/td&gt;
            &lt;td&gt;{{ item.toppings.toString() }}&lt;/td&gt;
            &lt;td&gt;{{ item.price + item.toppings.length * 10 }}&lt;/td&gt;
            &lt;td&gt;{{ item.count }}&lt;/td&gt;
            &lt;td class="text-end"&gt;{{ item.total }}&lt;/td&gt;
          &lt;/tr&gt;
        &lt;/tbody&gt;
      &lt;/table&gt;
      &lt;p class="mt-3 mb-1"&gt;訂單成立時間：{{ checkedOrder.time }}&lt;/p&gt;
      &lt;p class="mb-1"&gt;餐點數： {{ checkedOrder.orders.length }}&lt;/p&gt;
      &lt;p class="mb-1"&gt;付款狀態：未付款&lt;/p&gt;
      &lt;p class="text-end mt-auto"&gt;共 NT$ {{ checkedOrder.total }} 元&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const app = {
  // 資料 (函式)
  data() {
    return {
      itemSelected: {},
      orderList: &#91;],
      orderTotal: 0,
      // 確認訂單
      checkedOrder: {
        time: "",
        total: 0,
        orders: &#91;],
      },
      iceType: &#91;"正常冰", "少冰", "微冰", "去冰", "熱"],
      sugarType: &#91;"全糖", "七分", "半糖", "三分", "無糖"],
      toppingsType: &#91;"珍珠", "粉條", "小粉圓", "椰果", "芋頭"],
      products: &#91;
        {
          name: "珍珠鮮奶茶",
          engName: "Pearl Milk Tea",
          price: 60,
          defaults: {
            toppings: &#91;"珍珠"],
            sugar: "",
            ice: "",
          },
        },
        {
          name: "椰果鮮奶茶",
          engName: "Coconut Milk Tea",
          price: 60,
          defaults: {
            toppings: &#91;"椰果"],
            sugar: "",
            ice: "",
          },
        },
        {
          name: "鮮奶茶",
          engName: "Milk Tea",
          price: 50,
          defaults: {
            toppings: &#91;""],
            sugar: "",
            ice: "",
          },
        },
        {
          name: "古意冬瓜茶 (糖固定)",
          engName: "Winter Melon Drink",
          price: 30,
          defaults: {
            toppings: &#91;""],
            sugar: "全糖",
            ice: "",
          },
        },
        {
          name: "蜜香紅茶",
          engName: "Black Tea",
          price: 30,
          defaults: {
            toppings: &#91;""],
            sugar: "",
            ice: "",
          },
        },
        {
          name: "包種青茶",
          engName: "Pouchong tea",
          price: 35,
          defaults: {
            toppings: &#91;""],
            sugar: "",
            ice: "",
          },
        },
        {
          name: "檸檬烏龍",
          engName: "Lemon Oolong Tea",
          price: 55,
          defaults: {
            toppings: &#91;""],
            sugar: "",
            ice: "",
          },
        },
        {
          name: "薑母茶 (熱飲)",
          engName: "Ginger Tea",
          price: 55,
          defaults: {
            toppings: &#91;""],
            sugar: "",
            ice: "熱",
          },
        },
        {
          name: "青草茶",
          engName: "Herbal Tea",
          price: 35,
          defaults: {
            toppings: &#91;""],
            sugar: "",
            ice: "",
          },
        },
        {
          name: "金桔檸檬",
          engName: "Kumquat Lemonade",
          price: 40,
          defaults: {
            toppings: &#91;""],
            sugar: "",
            ice: "",
          },
        },
        {
          name: "柳澄青茶",
          engName: "Orange Mountain Tea",
          price: 45,
          defaults: {
            toppings: &#91;""],
            sugar: "",
            ice: "",
          },
        },
      ],
    };
  },
  // 生命週期 (函式)
  created() {
    console.log(this);
  },
  // 方法 (物件)
  methods: {
    // 選擇飲品
    selectProduct(product) {
      this.itemSelected = {
        ...product,
        count: 1,
        toppings: &#91;],
        // 三元運算子
        ice: product.defaults.ice !== "" ? product.defaults.ice : "正常冰",
        sugar:
          product.defaults.sugar !== "" ? product.defaults.sugar : "全糖",
        notice: "",
      };
    },
    // 加入品項
    addToOrder(product) {
      const order = {
        ...product,
        total: (product.price + product.toppings.length * 10) * product.count,
      };
      this.orderList.push(order);
      this.countTotal();
      this.resetOrder();
    },
    // 計算總價
    countTotal() {
      // reduce 將陣列化為單一值
      this.orderTotal = this.orderList.reduce(
        (acc, obj) =&gt; acc + obj.total,
        0
      );
    },
    resetOrder() {
      this.itemSelected = {};
    },
    // 產生訂單
    generateOrder(orders, total) {
      const date = new Date().toLocaleString();
      this.checkedOrder.time = date;
      this.checkedOrder.orders = orders;
      this.checkedOrder.total = total;
      this.orderList = &#91;];
      this.resetOrder();
    },
  },
};

Vue.createApp(app).mount("#app");</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Vue3 複習02</title>
		<link>/wordpress_blog/reviewvue3_02/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Thu, 22 Feb 2024 08:39:00 +0000</pubDate>
				<category><![CDATA[六角學院]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=815</guid>

					<description><![CDATA[快速入門 Vue.js: 商品後台管理介面 MVVM 概念介紹  [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">快速入門 Vue.js: 商品後台管理介面</h2>



<h3 class="wp-block-heading">MVVM 概念介紹</h3>



<h3 class="wp-block-heading">Vue.js 起手式</h3>



<p>環境安裝 – Vue.js devtools</p>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  {{ counter }} 
  {{ text }}
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
Vue.createApp({
  data() {
    return {
      counter: 5,
      text: "這裡有一段文字",
    };
  },
}).mount("#app");
</code></pre>



<h3 class="wp-block-heading">起手常見結構</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  {{ counter }}
  &lt;button type="button" v-on:click="clickMe"&gt;按我&lt;/button&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const app = {
  // 資料 (函式)
  data() {
    return {
      counter: 0,
    };
  },
  // 生命週期 (函式)
  created() {
    this.counter = 10;
    console.log(this);
  },
  // 方法 (物件)
  methods: {
    clickMe() {
      // console.log(1);
      this.counter = this.counter + 1;
    },
  },
};

Vue.createApp(app).mount("#app");
</code></pre>



<h3 class="wp-block-heading">雙向綁定的技巧</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;form&gt;
    {{ temp }}
    &lt;div class="mb-3"&gt;
      &lt;label for="productName" class="form-label"&gt;產品名稱&lt;/label&gt;
      &lt;input
        type="text"
        id="productName"
        class="form-control"
        v-model="temp.name"
      /&gt;
    &lt;/div&gt;
    &lt;div class="mb-3"&gt;
      &lt;!-- HTML 屬性 --&gt;
      &lt;img v-bind:src="temp.imageUrl" class="img-fluid" alt="" /&gt;
      &lt;label for="productImage" class="form-label"&gt;產品圖片&lt;/label&gt;
      &lt;input
        type="text"
        id="productImage"
        class="form-control"
        v-model="temp.imageUrl"
      /&gt;
    &lt;/div&gt;
    &lt;button type="button" class="btn btn-secondary" v-on:click="confirmEdit"&gt;
      更新
    &lt;/button&gt;
  &lt;/form&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const App = {
  data() {
    return {
      temp: {
        name: "筆電",
        imageUrl:
          "https://images.unsplash.com/photo-1602526430780-782d6b1783fa?ixid=MXwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=1350&amp;q=80",
      },
    };
  },
  methods: {
    confirmEdit() {
      console.log(this.temp);
    },
  },
};

Vue.createApp(App).mount("#app");
</code></pre>



<h3 class="wp-block-heading">將資料加入於 Vue Data</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;form&gt;
    &lt;div class="mb-3"&gt;
      &lt;label for="productName" class="form-label"&gt;產品名稱&lt;/label&gt;
      &lt;input
        type="text"
        id="productName"
        class="form-control"
        v-model="temp.name"
      /&gt;
    &lt;/div&gt;
    &lt;div class="mb-3"&gt;
      &lt;img :src="temp.imageUrl" class="img-fluid d-block" alt="" width="300" /&gt;
      &lt;label for="productImage" class="form-label"&gt;產品圖片&lt;/label&gt;
      &lt;input
        type="text"
        id="productImage"
        class="form-control"
        v-model="temp.imageUrl"
      /&gt;
    &lt;/div&gt;
    &lt;button type="button" class="btn btn-secondary" v-on:click="confirmEdit"&gt;
      更新
    &lt;/button&gt;
  &lt;/form&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const products = &#91;
  {
    id: "1",
    imageUrl:
      "https://images.unsplash.com/photo-1516906571665-49af58989c4e?ixlib=rb-1.2.1&amp;ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;auto=format&amp;fit=crop&amp;w=300&amp;q=80",
    name: "MacBook Pro",
    onStock: false,
  },
  {
    id: "2",
    imageUrl:
      "https://images.unsplash.com/photo-1512499617640-c74ae3a79d37?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=300&amp;q=80",
    name: "iPhone",
    onStock: false,
  },
];
const App = {
  data() {
    return {
      temp: {
        name: "筆電",
        imageUrl:
          "https://images.unsplash.com/photo-1602526430780-782d6b1783fa?ixid=MXwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=1350&amp;q=80",
      },
      products: &#91;],
    };
  },
  methods: {
    confirmEdit() {
      this.temp.id = new Date().getTime(); // unix timestamp
      this.temp.onStock = false;
      // console.log(this.temp);
      this.products.push(this.temp); // 把 temp 加入到 products;
      this.temp = {};
    },
  },
  created() {
    this.products = products;
  },
};

Vue.createApp(App).mount("#app");
</code></pre>



<h3 class="wp-block-heading">簡單語法呈現大量資料於畫面上</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;table class="table"&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th&gt;標題&lt;/th&gt;
        &lt;th&gt;圖片&lt;/th&gt;
        &lt;th&gt;銷售狀態&lt;/th&gt;
        &lt;th&gt;編輯&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;!-- :key 補上唯一值 --&gt;
      &lt;tr
        v-for="item in products"
        key="item.id"
        v-bind:class="{ 'table-success': item.onStock }"
      &gt;
        &lt;td&gt;{{ item.name }}&lt;/td&gt;
        &lt;td&gt;
          &lt;img v-bind:src="item.imageUrl" width="300" alt="" /&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;input type="checkbox" v-model="item.onStock" /&gt;
          &lt;!-- {{ item.onStock }} --&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;button type="button" class="btn btn-outline-primary"&gt;編輯&lt;/button&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;form&gt;
    &lt;div class="mb-3"&gt;
      &lt;label for="productName" class="form-label"&gt;產品名稱&lt;/label&gt;
      &lt;input
        type="text"
        id="productName"
        class="form-control"
        v-model="temp.name"
      /&gt;
    &lt;/div&gt;
    &lt;div class="mb-3"&gt;
      &lt;img :src="temp.imageUrl" class="img-fluid d-block" alt="" width="300" /&gt;
      &lt;label for="productImage" class="form-label"&gt;產品圖片&lt;/label&gt;
      &lt;input
        type="text"
        id="productImage"
        class="form-control"
        v-model="temp.imageUrl"
      /&gt;
    &lt;/div&gt;
    &lt;button type="button" class="btn btn-secondary" v-on:click="confirmEdit"&gt;
      更新
    &lt;/button&gt;
  &lt;/form&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const products = &#91;
  {
    id: "1",
    imageUrl:
      "https://images.unsplash.com/photo-1516906571665-49af58989c4e?ixlib=rb-1.2.1&amp;ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;auto=format&amp;fit=crop&amp;w=300&amp;q=80",
    name: "MacBook Pro",
    onStock: false,
  },
  {
    id: "2",
    imageUrl:
      "https://images.unsplash.com/photo-1512499617640-c74ae3a79d37?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=300&amp;q=80",
    name: "iPhone",
    onStock: false,
  },
];
const App = {
  data() {
    return {
      products: &#91;],
      temp: {
        name: "卡斯伯",
        imageUrl:
          "https://images.unsplash.com/photo-1602526430780-782d6b1783fa?ixid=MXwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=1350&amp;q=80",
      },
    };
  },
  methods: {
    confirmEdit() {
      this.temp.id = new Date().getTime();
      this.temp.onStock = false;
      this.products.push(this.temp);
      this.temp = {};
    },
  },
  created() {
    this.products = products;
  },
};

Vue.createApp(App).mount("#app");
</code></pre>



<h3 class="wp-block-heading">編輯你的資料狀態</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;table class="table"&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th&gt;標題&lt;/th&gt;
        &lt;th&gt;圖片&lt;/th&gt;
        &lt;th&gt;銷售狀態&lt;/th&gt;
        &lt;th&gt;編輯&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr
        v-for="item in products"
        :key="item.id"
        :class="{'table-success': item.onStock}"
      &gt;
        &lt;td&gt;{{ item.name }}&lt;/td&gt;
        &lt;td&gt;
          &lt;img :src="item.imageUrl" alt="" height="100" /&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;input type="checkbox" v-model="item.onStock" /&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;button
            type="button"
            class="btn btn-outline-primary"
            v-on:click="editItem(item)"
          &gt;
            修改
          &lt;/button&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;form&gt;
    &lt;div class="mb-3"&gt;
      &lt;label for="productName" class="form-label"&gt;產品名稱&lt;/label&gt;
      &lt;input
        type="text"
        id="productName"
        class="form-control"
        v-model="temp.name"
      /&gt;
    &lt;/div&gt;
    &lt;div class="mb-3"&gt;
      &lt;img :src="temp.imageUrl" class="img-fluid d-block" alt="" width="300" /&gt;
      &lt;label for="productImage" class="form-label"&gt;產品圖片&lt;/label&gt;
      &lt;input
        type="text"
        id="productImage"
        class="form-control"
        v-model="temp.imageUrl"
      /&gt;
    &lt;/div&gt;
    &lt;button type="button" class="btn btn-secondary" v-on:click="confirmEdit"&gt;
      更新
    &lt;/button&gt;
  &lt;/form&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const products = &#91;
  {
    id: "1",
    imageUrl:
      "https://images.unsplash.com/photo-1516906571665-49af58989c4e?ixlib=rb-1.2.1&amp;ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;auto=format&amp;fit=crop&amp;w=300&amp;q=80",
    name: "MacBook Pro",
    onStock: false,
  },
  {
    id: "2",
    imageUrl:
      "https://images.unsplash.com/photo-1512499617640-c74ae3a79d37?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=300&amp;q=80",
    name: "iPhone",
    onStock: false,
  },
];
const App = {
  data() {
    return {
      products: &#91;],
      temp: {
        name: "筆電",
        imageUrl:
          "https://images.unsplash.com/photo-1602526430780-782d6b1783fa?ixid=MXwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=1350&amp;q=80",
      },
    };
  },
  methods: {
    confirmEdit() {
      if (!this.temp.id) {
        // 新增資料
        this.temp.id = new Date().getTime();
        this.temp.onStock = false;
        this.products.push(this.temp);
        this.temp = {};
      } else {
        this.products.forEach((item, i) =&gt; {
          if (item.id === this.temp.id) {
            this.products&#91;i] = this.temp;
          }
        });
        this.temp = {};
      }
    },
    editItem(item1) {
      // console.log("editItem", item1);

      // 陷阱
      // this.temp = item1;

      // 淺層拷貝
      this.temp = { ...item1 };
    },
  },
  created() {
    this.products = products;
  },
};

Vue.createApp(App).mount("#app");
</code></pre>



<h3 class="wp-block-heading">基礎章節作業: 完成新增、編輯商品項目</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div id="app"&gt;
  &lt;div class="text-end"&gt;
    &lt;button class="btn btn-primary" type="button" v-on:click="addItem"&gt;
      新增
    &lt;/button&gt;
  &lt;/div&gt;
  &lt;table class="table"&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th&gt;標題&lt;/th&gt;
        &lt;th&gt;圖片&lt;/th&gt;
        &lt;th&gt;銷售狀態&lt;/th&gt;
        &lt;th&gt;編輯&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr
        v-for="item in products"
        :key="item.id"
        :class="{'table-success': item.onStock}"
      &gt;
        &lt;td&gt;{{ item.name }}&lt;/td&gt;
        &lt;td&gt;
          &lt;img :src="item.imageUrl" alt="" height="100" /&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;input type="checkbox" v-model="item.onStock" /&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;button
            type="button"
            class="btn btn-outline-primary"
            v-on:click="editItem(item)"
          &gt;
            修改
          &lt;/button&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;form v-if="isNew || temp.id"&gt;
    &lt;div class="mb-3"&gt;
      &lt;label for="productName" class="form-label"&gt;產品名稱&lt;/label&gt;
      &lt;input
        type="text"
        id="productName"
        class="form-control"
        v-model="temp.name"
      /&gt;
    &lt;/div&gt;
    &lt;div class="mb-3"&gt;
      &lt;label for="productImage" class="form-label"&gt;產品圖片&lt;/label&gt;
      &lt;input
        type="text"
        id="productImage"
        class="form-control"
        v-model="temp.imageUrl"
      /&gt;
    &lt;/div&gt;
    &lt;button type="button" class="btn btn-secondary" v-on:click="confirmEdit"&gt;
      更新
    &lt;/button&gt;
  &lt;/form&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS
const products = &#91;
  {
    id: "1",
    imageUrl:
      "https://images.unsplash.com/photo-1516906571665-49af58989c4e?ixlib=rb-1.2.1&amp;ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;auto=format&amp;fit=crop&amp;w=300&amp;q=80",
    name: "MacBook Pro",
    onStock: false,
  },
  {
    id: "2",
    imageUrl:
      "https://images.unsplash.com/photo-1512499617640-c74ae3a79d37?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=300&amp;q=80",
    name: "iPhone",
    onStock: false,
  },
];
const App = {
  data() {
    return {
      products: &#91;],
      temp: {},
      // 狀態，決定是否為新增產品
      isNew: false,
    };
  },
  methods: {
    editItem(item) {
      this.temp = { ...item };
    },
    confirmEdit() {
      if (!this.temp.id) {
        this.temp.id = new Date().getTime();
        this.temp.onStock = false;
        this.products.push(this.temp);
      } else {
        this.products.forEach((item, i) =&gt; {
          if (item.id === this.temp.id) {
            this.products&#91;i] = this.temp;
          }
        });
      }
      this.temp = {};
      this.isNew = false;
    },
    addItem() {
      this.isNew = true;
      this.temp = {};
    },
  },
  created() {
    this.products = products;
  },
};

Vue.createApp(App).mount("#app");</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Vue3 複習01</title>
		<link>/wordpress_blog/reviewvue3_01/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Sat, 17 Feb 2024 07:28:00 +0000</pubDate>
				<category><![CDATA[六角學院]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=812</guid>

					<description><![CDATA[學習來自: 六角學院課程: Vue 3 實戰影音課程Thank  [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>學習來自: 六角學院<br>課程: Vue 3 實戰影音課程<br>Thank you</p>



<h2 class="wp-block-heading">課前章節 – JS 必備觀念</h2>



<h3 class="wp-block-heading">JS 額外補充資源</h3>



<p>為了加強學習 Vue 前的基礎知識</p>



<p>除了 本課程中的「課前章節 – JS 必備觀念」</p>



<p>除了課綱內的內容外，同時可參考以下資源</p>



<ul class="wp-block-list">
<li><a href="https://www.youtube.com/watch?v=_vFuDQ_6Xt8" target="_blank" rel="noreferrer noopener">JavaScript 陣列處理必學巧技</a></li>



<li><a href="https://www.youtube.com/watch?v=FGdKdn_CnWo" target="_blank" rel="noreferrer noopener">JavaScript 那個 let, const, var 到底差在哪?</a></li>



<li><a href="https://www.youtube.com/watch?v=y1odVMpi6dU" target="_blank" rel="noreferrer noopener">JavaScript 常見考題破解:物件傳值?</a></li>
</ul>



<h3 class="wp-block-heading">課程環境</h3>



<h3 class="wp-block-heading">課程相關資源</h3>



<h4 class="wp-block-heading">課程練習手冊:</h4>



<p><a rel="noreferrer noopener" href="https://github.com/hexschool/vue3-starter-files/tree/gh-pages" target="_blank">連結</a></p>



<h4 class="wp-block-heading">Chrome 必裝的 Vue 開發套件:</h4>



<p><a href="https://devtools.vuejs.org/guide/installation.html" target="_blank" rel="noreferrer noopener">Vue Devtools</a></p>



<h4 class="wp-block-heading">課程使用的 CSS 函式庫 Bootstrap:</h4>



<p><a href="https://bootstrap5.hexschool.com/" target="_blank" rel="noreferrer noopener">六角學院翻譯中文版</a></p>



<h4 class="wp-block-heading">VSCode 相關套件:</h4>



<p><a href="https://marketplace.visualstudio.com/items?itemName=akamud.vscode-theme-onedark" target="_blank" rel="noreferrer noopener">講師使用的佈景主題</a></p>



<p><a href="https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer" target="_blank" rel="noreferrer noopener">Live Server</a></p>



<p><a href="https://marketplace.visualstudio.com/items?itemName=octref.vetur" target="_blank" rel="noreferrer noopener">Vue 官方提供的 Vue 整合插件</a></p>



<p><a href="https://marketplace.visualstudio.com/items?itemName=hollowtree.vue-snippets" target="_blank" rel="noreferrer noopener">Vue 3 Snippets</a></p>



<p><a href="https://marketplace.visualstudio.com/items?itemName=MS-CEINTL.vscode-language-pack-zh-hant" target="_blank" rel="noreferrer noopener">繁體中文版</a></p>



<p><a href="https://wcc723.github.io/development/2019/12/01/vscode-chinese/" target="_blank" rel="noreferrer noopener">中文版安裝說明</a></p>



<p><a href="https://wcc723.github.io/development/2020/12/13/vscode-extension/" target="_blank" rel="noreferrer noopener">其他推薦 VSCode 套件</a></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>關於 Vue dev tools 的常見問題，有幾個地方可以檢查看看</p>



<ul class="wp-block-list">
<li>需確保當前頁面一定要有 createApp 的程式碼 (如果當頁有，可以另開新分頁確認，有時候重新整理也不會顯示)</li>



<li>Vue 並非使用開發版本 (課程中是使用開發版本)</li>



<li>dev tools 並非對應 Vue 3 的版本，請確認是否使用 dev 版本 (兩者僅能擇一開啟)</li>
</ul>



<p><a href="https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd" target="_blank" rel="noreferrer noopener">連結</a></p>



<p>由於這是 Vue3 的版本插件，如果未來需要用舊版的插件</p>



<p>可以從 Chrome 的後台進行切換</p>



<h3 class="wp-block-heading">JS 常見縮寫</h3>



<pre class="wp-block-code"><code>// HTML
&lt;ul&gt;
  &lt;li&gt;1&lt;/li&gt;
  &lt;li&gt;2&lt;/li&gt;
  &lt;li&gt;3&lt;/li&gt;
&lt;/ul&gt;</code></pre>



<pre class="wp-block-code"><code>// JS
// #1 語法糖與新增語法
// 語法糖：不會影響運作，邏輯與當前 JS 一致
// const obj = {
//   myName: "1. 物件",
//   fn() {
//     return this.myName;
//   },
// };

// console.log(obj.fn());

// =====
// #2 物件字面值 Object literals
// #2-1 物件內的函式
// const obj3 = {
//   myName: "物件",
//   fn() {
//     // 請改為縮寫
//     return this.myName;
//   },
// };

// console.log(obj3.fn());

// #2-2 物件內的變數
// const person = {
//   name: "小明",
// };

// const people = {
//   person,
// };

// console.log(people);

// #3 展開
// #3-1 不同陣列合併
// const groupA = &#91;"小明", "杰倫", "阿姨"];
// const groupB = &#91;"老媽", "老爸"];
// // const groupAll = groupA.concat(groupB);
// const groupAll = &#91;...groupA, ...groupB];

// console.log(groupAll);

// #3-2 物件擴展
// 新增一個物件包含新方法，同時加入原有的方法
// const methods = {
//   fn1() {
//     console.log(1);
//   },
//   fn2() {
//     console.log(1);
//   },
// };

// const newMethods = {
//   fn() {
//     console.log(1);
//   },
//   ...methods,
// };
// console.log(newMethods);

// #3-3 轉成純陣列
// const doms = document.querySelectorAll("li");
// console.log(doms); // 請轉為純陣列
// const newDoms = &#91;...doms];
// console.log(newDoms);

// #4 預設值
function sum(a, b = 2) {
  // 請加入預設值避免錯誤
  return a + b;
}
console.log(sum(1, 3));
</code></pre>



<h3 class="wp-block-heading">This 的運作</h3>



<pre class="wp-block-code"><code>// JS
// #1 一個函式中包含多少參數
// var a = "全域";
// function fn(params) {
//   console.log(params, this, window, arguments);
//   debugger;
// }
// fn(1, 2, 3);

// #2 this 的指向為何
// var obj = {
//   name: "小明",
//   fn: function (params) {
//     console.log(params, this, window, arguments);
//     // debugger;
//   },
// };
// obj.fn(1, 2, 3);

// #3 注意：this 的指向相當複雜，大部分情境只需要了解其中一種即可(95%)
// 傳統函式中的 this 只與調用方式有關
var someone = "全域";
function callSomeone() {
  console.log(this.someone);
}
// callSomeone(); // simple call

// #4 各種運用變化
// var obj = {
//   someone: "物件",
//   callSomeone() {
//     console.log(this.someone);
//   },
// };
// obj.callSomeone();

// var obj2 = {
//   someone: "物件2",
//   callSomeone,
// };
// obj2.callSomeone();

// var wrapObj = {
//   someone: "外層物件",
//   callSomeone,
//   innerObj: {
//     someone: "內層物件",
//     callSomeone,
//   },
// };
// wrapObj.callSomeone(); // 指向外層物件
// wrapObj.innerObj.callSomeone(); // 指向內層物件

// var obj3 = {
//   someone: "物件 3",
//   fn() {
//     callSomeone(); // 通常平常不會這樣去取用 this
//   },
// };
// obj3.fn();

var obj4 = {
  someone: "物件 4",
  fn() {
    setTimeout(function () {
      console.log(this.someone);
    });
  },
};
obj4.fn();
</code></pre>



<h3 class="wp-block-heading">箭頭函式</h3>



<pre class="wp-block-code"><code>// JS
// #1 箭頭函式的縮寫
// const arr = &#91;1, 2, 3, 4, 5];

// // const filterArr = arr.filter(function (item) {
// //   // console.log(item);
// //   // console.log(item % 2);
// //   return item % 2; // 結果為真值
// // });
// const filterArr = arr.filter((item) =&gt; item % 2); // 取有餘數的值（單數）
// // 箭頭函式會自動帶 return
// console.log(filterArr);

// #2 This 綁定的差異
// #2-1 活用觀念，將內層的改為箭頭
// var name = "全域";
// const person = {
//   name: "小明",
//   callName: function () {
//     console.log("1", this.name); // 1 小明
//     // setTimeout(function () {
//     //   console.log("2", this.name); // 2
//     //   console.log("3", this); // 3
//     // }, 10);
//     // 箭頭函式，沒有自己的 this
//     setTimeout(() =&gt; {
//       console.log("2", this.name); // 2
//       console.log("3", this); // 3
//     }, 10);
//   },
// };
// person.callName();

// #3 陷阱
// #3-1
// var name = "全域";
// const person = {
//   name: "小明",
//   callName: () =&gt; {
//     console.log(this.name); // 請尋找箭頭所在的作用域為何？
//   },
// };
// person.callName();

// #3-2
// var name = "全域";
// const person = {
//   name: "小明",
//   callMe() {
//     const callName = () =&gt; {
//       console.log(this.name); // 請尋找箭頭所在的作用域為何？
//     };
//     callName();
//   },
// };
// person.callMe();

// #4 實戰手法
var someone = "全域";
// function callSomeone() {
//   console.log(this.someone);
// }

// 1. this 先指向其他變數
// 2. 使用箭頭函式
var obj4 = {
  someone: "物件 4",
  fn() {
    // 方法一
    // const vm = this; // vm 在 Vue 中意指 ViewModel
    // setTimeout(function () {
    //   // callback function
    //   // console.log(this.someone);
    //   console.log(vm.someone);
    // });
    // 方法二
    setTimeout(() =&gt; {
      console.log(this.someone);
    });
  },
};

obj4.fn();
</code></pre>



<h3 class="wp-block-heading">關注點分離概念說明</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div class="component"&gt;
  &lt;ul&gt;&lt;/ul&gt;
  &lt;input type="text" class="inputData"&gt;
  &lt;button type="button" class="addBtn"&gt;增加內容&lt;/button&gt;
&lt;/div&gt;</code></pre>



<pre class="wp-block-code"><code>// JS
// #1 資料、畫面、方法分離
// 畫面 = html
// 資料 = component.data
// 方法 = 物件內的其它函式

// #2 元件結構
// 1. 資料
// 2. 方法、觸發器
// 3. 生命週期（初始化）
const component = {
  data: &#91; // 資料
    '這是第一句話',
    '這是第二句話',
    '這是第三句話'
  ],
  removeData(id) {
    this.data.splice(id, 1);
    this.render();
  },
  render() { // 渲染方法
    const list = document.querySelector('.component ul');
    let content = '';
    this.data.forEach((item, i) =&gt; {
      content = `${content}&lt;li&gt;${item} &lt;button type="button" class="btn" data-id="${i}"&gt;移除&lt;/button&gt;&lt;/li&gt;`
    });
    // 縮寫優化
    // const content = component.data.map(item =&gt; `&lt;li&gt;${item}&lt;/li&gt;`).join('');
    list.innerHTML = content;

    // 加入監聽
    const btns = document.querySelectorAll('.btn');
    btns.forEach(btn =&gt; btn.addEventListener('click', (e)=&gt; {
      // #2 重點，移除項目是先移除資料，而不是直接移除 DOM
      // 如果要進行 AJAX 或更複雜行為，不會因為 DOM 與資料混合而難以運作
      const id = e.target.dataset.id;
      this.removeData(id)
    }))
  },
  init() {
    this.render();
  }
}
component.init();
</code></pre>



<p>傳統形式</p>



<ul class="wp-block-list">
<li>將文字提取</li>



<li>產生一個新的節點，並且帶入文字</li>
</ul>



<p>關注點分離</p>



<ul class="wp-block-list">
<li>將文字提取出，並且寫入資料集</li>



<li>資料集的內容，轉換成畫面</li>
</ul>



<h3 class="wp-block-heading">關注點分離實作概念</h3>



<pre class="wp-block-code"><code>// HTML
&lt;div class="component"&gt;
  &lt;ul&gt;&lt;/ul&gt;
&lt;/div&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS - 課程講解
// 畫面 - HTML
// 資料 - DATA
// 方法 - function

// #元件結構
// 1. 資料
// 2. 方法、觸發器
// 3. 生命週期
const component = {
  // 資料
  data: &#91;"這是第一句話", "這是第二句話", "這是第三句話"],
  // 事件觸發
  removeData(id) {
    // console.log(id);
    // console.log(this);
    this.data.splice(id, 1);
    this.render();
  },
  // 渲染方法
  render() {
    //
    // 方法一
    // const vm = this;
    const list = document.querySelector(".component ul");
    // console.log(list);
    let content = ""; // li 結構使用
    this.data.forEach((item, i) =&gt; {
      // console.log(item);
      content = `${content}&lt;li&gt;${item}
                    &lt;button type="button" class="btn" data-id="${i}"&gt;刪除&lt;/button&gt;&lt;/li&gt;`;
      // console.log(content);
    });
    list.innerHTML = content;

    const btns = document.querySelectorAll(".btn");
    // console.log(btns);
    btns.forEach((btn) =&gt;
      // btn.addEventListener("click", function (e) {
      //   // console.log(e);
      //   // console.log(e.target.dataset.id); // 陣列索引位置
      //   console.log(this);
      //   this.removeData(e.target.dataset.id);
      // })
      // 方法二
      // 把上面的 function 改成箭頭函式
      btn.addEventListener("click", (e) =&gt; {
        // console.log(e);
        // console.log(e.target.dataset.id); // 陣列索引位置
        console.log(this);
        this.removeData(e.target.dataset.id);
      })
    );
  },
  // 生命週期
  init() {
    this.render();
  },
};
component.init();
</code></pre>



<pre class="wp-block-code"><code>// JS - 範例
// #1 資料、畫面、方法分離
// 畫面 = html
// 資料 = component.data
// 方法 = 物件內的其它函式

// #2 元件結構
// 1. 資料
// 2. 方法、觸發器
// 3. 生命週期（初始化）
// const component = {
//   data: &#91; // 資料
//     '這是第一句話',
//     '這是第二句話',
//     '這是第三句話'
//   ],
//   removeData(id) {
//     this.data.splice(id, 1);
//     this.render();
//   },
//   render() { // 渲染方法
//     const list = document.querySelector('.component ul');
//     let content = '';
//     this.data.forEach((item, i) =&gt; {
//       content = `${content}&lt;li&gt;${item} &lt;button type="button" class="btn" data-id="${i}"&gt;移除&lt;/button&gt;&lt;/li&gt;`
//     });
//     // 縮寫優化
//     // const content = component.data.map(item =&gt; `&lt;li&gt;${item}&lt;/li&gt;`).join('');
//     list.innerHTML = content;

//     // 加入監聽
//     const btns = document.querySelectorAll('.btn');
//     btns.forEach(btn =&gt; btn.addEventListener('click', (e)=&gt; {
//       // #2 重點，移除項目是先移除資料，而不是直接移除 DOM
//       // 如果要進行 AJAX 或更複雜行為，不會因為 DOM 與資料混合而難以運作
//       const id = e.target.dataset.id;
//       this.removeData(id)
//     }))
//   },
//   init() {
//     this.render();
//   }
// }
// component.init();
</code></pre>



<h3 class="wp-block-heading">傳參考特性 (常踩到的雷)</h3>



<p>因為關注點分離，畫面由框架來處理，開發者將專注於資料處理，因此資料處理的知識顯得格外重要。</p>



<pre class="wp-block-code"><code>// JS
// #1 物件是以傳參考的形式賦值
// const person = {
//   name: '小明',
//   obj: {}
// }

// const person2 = person;
// person2.name = '杰倫';
// // console.log(person2 === person);
// // console.log(person2, person);

// const obj2 = person.obj;
// obj2.name = "物件下的名稱";
// console.log(person);


// #2 陷阱
// const fn = (item) =&gt; {
//   item.name = '杰倫';
//   // ...
// }
// const person = {
//   name: '小明',
//   obj: {}
// }
// fn(person);
// console.log('person', person);
// // ...

// 關於：這樣的錯誤，ESLint 也有建議不要這麼做（但你是有經驗的開發者時，下方也有提供關閉的方式）
// https://cn.eslint.org/docs/rules/no-param-reassign

// #3 解決方案
// #3-1 淺層拷貝
// const person = {
//   name: '小明',
//   obj: {}
// }
// // 方法一
// const person2 = Object.assign({}, person);
// // 方法二
// const person3 = {
//   ...person
// };
// console.log(person === person2, person === person3);
// person2.obj.age = 16;
// console.log(person, person2);

// #3-2 深層拷貝
const person = {
  name: '小明',
  obj: {}
}
// 物件先轉成字串，然後再轉成物件
const person2 = JSON.parse(JSON.stringify(person));
// console.log(person2);
// console.log(person2 === person);
person2.obj.age = 16;
console.log(person2, person);
</code></pre>



<h3 class="wp-block-heading">Promise 非同步觀念</h3>



<pre class="wp-block-code"><code>// JS
// #1 非同步的觀念
// function getData() {
//   setTimeout(() =&gt; {
//     console.log("... 已取得遠端資料");
//   }, 0);
// }
// // 請問取得資料的順序為何
// const component = {
//   init() {
//     console.log(1);
//     getData();
//     console.log(2);
//   },
// };
// component.init();

// 更正確的說法，Promise 是為了解決傳統非同步語法難以建構及管理的問題，如有興趣可搜尋 "callback hell js"

// #2 Promise
// 在此不需要學習 Promise 的建構方式，僅需要了解如何運用即可
const promiseSetTimeout = (status) =&gt; {
  return new Promise((resolve, reject) =&gt; {
    setTimeout(() =&gt; {
      if (status) {
        resolve("promiseSetTimeout 成功");
      } else {
        reject("promiseSetTimeout 失敗");
      }
    }, 1000);
  });
};

// #2-1 基礎運用
// promiseSetTimeout(true).then(function (res) {
//   console.log(res);
// });

// #2-2 串接
// promiseSetTimeout(true)
//   .then(function (res) {
//     console.log(1, res);
//     return promiseSetTimeout(true);
//   })
//   .then((res) =&gt; {
//     console.log(2, res);
//   });

// #2-3 失敗捕捉
promiseSetTimeout(false)
  .then((res) =&gt; {
    console.log(res);
  })
  .catch((err) =&gt; {
    console.log(err);
  });

// #2-4 元件運用
const component = {
  data: {},
  init() {
    console.log(this);
    promiseSetTimeout(true).then((res) =&gt; {
      this.data.res = res;
      console.log(this.data);
    });
  },
};
component.init();
</code></pre>



<h3 class="wp-block-heading">使用 Axios 串接 API</h3>



<pre class="wp-block-code"><code>// HTML
&lt;!-- 本章節額外載入的遠端套件 --&gt;
&lt;script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"&gt;&lt;/script&gt;</code></pre>



<pre class="wp-block-code"><code>// JS
// #3 實戰取得遠端資料
// #3-1
// https://github.com/axios/axios
axios
  .get("https://randomuser.me/api/gg")
  .then((res) =&gt; {
    console.log(res.data.results);
  })
  .catch((err) =&gt; {
    // console.log(err);
    console.log(err.response);
  });

// #3-2 記得捕捉錯誤，Axios 錯誤捕捉技巧
// https://randomuser.me/
</code></pre>



<h3 class="wp-block-heading">在瀏覽器上運行 ES 模組</h3>



<p>匯入 (html, js)</p>



<pre class="wp-block-code"><code>// 預設匯入
import 自訂名稱 from ...
// 具名匯入
import { 具名名稱 } from ...
</code></pre>



<p>匯出 (js)</p>



<pre class="wp-block-code"><code>// 預設匯出: 每個檔案唯一
export default</code></pre>



<pre class="wp-block-code"><code>// 具名匯出: 每個檔案多個
export const xxx = ...</code></pre>



<pre class="wp-block-code"><code>// HTML
&lt;div class="component"&gt;
  &lt;ul&gt;&lt;/ul&gt;
&lt;/div&gt;

&lt;script type="module"&gt;
  // #1 匯出匯入本一體，先掌握匯出更容易理解匯入
  // #1-1 將標籤定義 &lt;script type="module"&gt;
  // #1-2 預設匯出：defaultExport.js
  // 常見的匯出方式，通常用於匯出物件，在 Vue 開發中可用來匯出元件

  // #1-3 具名匯出：namedExport.js
  // 可用於匯出已定義的變數、物件、函式，專案開發中通常用於 “方法匯出”
  // 第三方的框架、函式、套件很常使用具名定義 “方法”

  // #2 匯入方法
  // #2-1 預設匯入
  // 因為預設匯出沒有名字，所以可以為它命名
  import newComponent from "./export1.js";
  newComponent.init();

  // 注意：因為傳參考的特性，因此元件無法重複利用（Vue.js 中會解決此問題）

  // #2-2 具名匯入
  // 單一匯入（建議寫法）
  // import { a, b } from "./export2.js";
  // console.log(a);
  // b();

  // 全部匯入（不建議，錯誤較難發現）
  import * as all from "./export2.js";
  console.log(all.a);
  all.b();
  console.log(all.c(1, 2));

  // #3 SideEffect
  // sideEffect.js（沒有包含匯出的檔案）
  import "./sideEffect.js";
  console.log($);
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// JS - export1.js
export default {
  data: &#91;
    // 資料
    "這是第一句話",
    "這是第二句話",
    "這是第三句話",
  ],
  removeData(id) {
    this.data.splice(id, 1);
    this.render();
  },
  render() {
    // 渲染方法
    const list = document.querySelector(".component ul");
    let content = "";
    this.data.forEach((item, i) =&gt; {
      content = `${content}&lt;li&gt;${item} &lt;button type="button" class="btn" data-id="${i}"&gt;移除&lt;/button&gt;&lt;/li&gt;`;
    });
    // 縮寫優化
    // const content = component.data.map(item =&gt; `&lt;li&gt;${item}&lt;/li&gt;`).join('');
    list.innerHTML = content;

    // 加入監聽
    const btns = document.querySelectorAll(".btn");
    btns.forEach((btn) =&gt;
      btn.addEventListener("click", (e) =&gt; {
        // #2 重點，移除項目是先移除資料，而不是直接移除 DOM
        // 如果要進行 AJAX 或更複雜行為，不會因為 DOM 與資料混合而難以運作
        const id = e.target.dataset.id;
        this.removeData(id);
      })
    );
  },
  init() {
    this.render();
  },
};
</code></pre>



<pre class="wp-block-code"><code>// JS -export2.js
export const a = 1;

export function b() {
  console.log(1);
}

export function c(a, b) {
  return a + b;
}
</code></pre>



<pre class="wp-block-code"><code>// JS - sideEffect.js
// 立即函式
(function (global) {
  global.$ = "我是 jQuery";
})(window);
</code></pre>



<h3 class="wp-block-heading">現行的 ES 模組使用技巧</h3>



<pre class="wp-block-code"><code>// HTML
&lt;!-- &lt;script type="module"&gt;
  // #1 每一個 type="module" 的作用域都是獨立的

  var a = 1;
  window.b = 2;
&lt;/script&gt;
&lt;script type="module"&gt;
  console.log(b);
&lt;/script&gt; --&gt;

&lt;div id="app"&gt;{{ counter }}&lt;/div&gt;
&lt;script type="module"&gt;
  // #2 可以運用的 ESM 套件
  // 網路上找到的 “ESM”，如果條件允許是可以使用 import 方式載入
  // https://cdnjs.com/libraries/vue
  // 比對語法：https://v3.vuejs.org/guide/introduction.html#declarative-rendering
  import { createApp } from "https://cdnjs.cloudflare.com/ajax/libs/vue/3.4.19/vue.esm-browser.min.js";

  const Counter = {
    data() {
      return {
        counter: 0,
      };
    },
  };

  createApp(Counter).mount("#app");
&lt;/script&gt;
</code></pre>



<h2 class="wp-block-heading">JS 必備觀念 – 課後測驗</h2>



<h3 class="wp-block-heading">this</h3>



<h3 class="wp-block-heading">物件參考特性</h3>



<h3 class="wp-block-heading">Promise</h3>



<h3 class="wp-block-heading">ESModule</h3>



<h3 class="wp-block-heading">縮寫</h3>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
