keyboard_tabSkip to content
listVue Firebase Masterclass

ทำหน้า Product Management

สารบัญ

ทำ stores/admin/product.js สำหรับลองส่งข้อมูลระหว่างหน้า

เราจะสร้าง store สำหรับจัดการ product ออกมาก่อนโดยการสร้างไฟล์ stores/admin/product.js

สิ่งที่จะทำ

  • เพิ่ม state list สำหรับเก็บ product ทั้งหมด, loaded สำหรับตัวแปรบอกว่า load ข้อมูลเรียบร้อย
  • สร้าง actions ขึ้นมาโดย
    • loadProduct () สำหรับดึงข้อมูล product ทั้งหมด จาก localstorage และ save ใส่ state list
    • getProduct (index) ดึงข้อมูล product จาก list
    • addProduct (productData) สำหรับ add ข้อมูล product เข้า list และ save ลง localstorage
    • updateProduct (index, productData) สำหรับ update ข้อมูล product เข้า list และ save ลง localstorage
    • removeProduct (index) สำหรับลบข้อมูล product จาก list และ save กลับ localstorage
javascript
import { defineStore } from 'pinia'

export const useProductStore = defineStore('product', {
  state: () => ({
    list: []
  }),
  actions: {
    loadProduct () {
      const productList = localStorage.getItem('product-data')
      if (productList) {
        this.list = JSON.parse(productList)
      }
    },
    getProduct (index) {
      return this.list[index]
    },
    addProduct (productData) {
      productData.remainQuantity = productData.quantity
      this.list.push(productData)
      // save to localstorage
      localStorage.setItem('product-data', JSON.stringify(this.list))
    },
    updateProduct (index, productData) {
      this.list[index].name = productData.name
      this.list[index].imageUrl = productData.imageUrl
      this.list[index].quantity = productData.quantity
      this.list[index].remainQuantity = productData.quantity
      this.list[index].status = productData.status
      this.list[index].updatedAt = (new Date).toLocaleString()
      // save to localstorage
      localStorage.setItem('product-data', JSON.stringify(this.list))
    },
    removeProduct (index) {
      this.list.splice(index, 1)
      // save to localstorage
      localStorage.setItem('product-data', JSON.stringify(this.list))
    }
  }
})

เพิ่ม router product

เพิ่ม path สำหรับ file product ListView, UpdateView

javascript
import AdminProductList from '@/views/admin/product/ListView.vue'
import AdminProductUpdate from '@/views/admin/product/UpdateView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/admin/products',
      name: 'admin-products',
      component: AdminProductList
    },
    {
      path: '/admin/products/create',
      name: 'admin-products-create',
      component: AdminProductUpdate
    },
    {
      path: '/admin/products/edit/:id',
      name: 'admin-products-update',
      component: AdminProductUpdate
    },
  ]
})

ทำ product/ListView.vue

ทำหน้าสำหรับแสดง Product ทั้งหมดออกมาเป็นตาราง

สิ่งที่จะทำ

  • ทำ style ตารางแสดงข้อมูล product ออกมา โดยแสดงข้อมูล ชื่อ product, ภาพ product, ราคา, จำนวน และ สถานะการเปิดปิด พร้อมกับเวลาล่าสุดที่ update
  • เพิ่ม ปุ่มสำหรับไปหน้าเพิ่ม Product
  • เพิ่ม icon สำหรับ Edit (สำหรับไปหน้าแก้ไข Product) และ Delete (สำหรับลบ Product)
  • เพิ่ม action ให้ตอนกด Delete สามารถลบ Product ได้
html
<script setup>
import { useProductStore } from '@/stores/admin/product'
import { useEventStore } from '@/stores/event'

import { RouterLink } from 'vue-router'

import AdminLayout from '@/layouts/AdminLayout.vue'
import TrashIcon from '@/components/icons/Trash.vue'
import EditIcon from '@/components/icons/Edit.vue'

const productStore = useProductStore()
const eventStore = useEventStore()

const removeProduct = (index) => {
  productStore.removeProduct(index)
  eventStore.popupMessage('success', 'DELETE Successful!')
}
</script>
<template>
  <AdminLayout>
    <div class="flex-1  pt-8 px-6 bg-base-100">
      <div class="card w-full p-6 mt-2">
        <div class="text-xl font-semibold inline-block">
          Product
          <div class="inline-block float-right">
            <div class="inline-block float-right">
              <RouterLink
                to="/admin/products/create"
                class="btn px-6 btn-sm normal-case btn-primary"
              >
                Add New
              </RouterLink>
            </div>
          </div>
        </div>
        <div class="divider mt-2"></div>
        <div class="h-full w-full pb-6 bg-base-100">
          <div class="overflow-x-auto w-full">
            <table class="table w-full">
              <thead>
                <tr>
                  <th>Name</th>
                  <th>Image</th>
                  <th>Price</th>
                  <th>Quantity</th>
                  <th>Status</th>
                  <th>Updated At</th>
                  <th></th>
                </tr>
              </thead>
              <tbody>
                <tr v-for="(product, index) in productStore.list" :key="index">
                  <td>
                    <div class="font-bold">{{ product.name }}</div>
                  </td>
                  <td>
                    <div class="mask mask-squircle w-12 h-12">
                      <img :src="product.imageUrl" />
                    </div>
                  </td>
                  <td>{{ product.price }}</td>
                  <td>{{ product.remainQuantity }} / {{ product.quantity }}</td>
                  <td>
                    <div class="badge" :class="product.status === 'open' ? 'badge-success' : 'badge-error'">
                      {{ product.status }}
                    </div>
                  </td>
                  <td>{{ product.updatedAt }}</td>
                  <td>
                    <RouterLink :to="{ name: 'admin-products-update', params: { id: index } }">
                      <button class="btn btn-square btn-ghost">
                        <EditIcon></EditIcon>
                      </button>
                    </RouterLink>
                    <button @click="removeProduct(index)" class="btn btn-square btn-ghost">
                      <TrashIcon></TrashIcon>
                    </button>
                  </td>
                </tr>
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </div>
  </AdminLayout>
</template>

ผลลัพธ์

product-01

ทำ product/UpdateView.vue

ทำหน้าสำหรับสร้างและแก้ไข Product

สิ่งที่ต้องทำ

  • เพิ่ม style สำหรับหน้าสร้าง Product ขึ้นมาโดยประกอบไปด้วย form
    • ชื่อ product เป็น text
    • ภาพ product เป็น url ภาพ (text)
    • ราคา เป็นตัวเลข (number)
    • จำนวน เป็นตัวเลข (number)
    • สถานะการเปิด / ปิด (text)
  • ตอนกด Add = ให้เกิด action เพิ่ม product
  • โดยถ้ามี parameter id ผ่าน router มา = ให้เปลี่ยนเป็นแก้ไขข้อมูลแทน (edit product) แทน
html
<script setup>
import { onMounted, ref, reactive, computed } from 'vue'
import { useProductStore } from '@/stores/admin/product'
import { useEventStore } from '@/stores/event'
import { RouterLink, useRoute, useRouter } from 'vue-router'

import AdminLayout from '@/layouts/AdminLayout.vue'

const productStore = useProductStore()
const eventStore = useEventStore()

const route = useRoute()
const router = useRouter()

const productId = ref(-1)
let selectedProduct = reactive({
  name: '',
  imageUrl: '',
  quantity: 0,
  about: '',
  status: 'open'
})

const mode = computed(() => {
  return productId.value !== -1 ? 'Edit' : 'Add'
})

onMounted(() => {
  if (route.params.id) {
    productId.value = route.params.id
    selectedProduct = productStore.getProduct(productId.value)
  }
})

const updateProduct = () => {
  if (productId.value !== -1) {
    // Edit mode
    productStore.updateProduct(productId.value, selectedProduct)
    eventStore.popupMessage('success', 'Update Product successful!')
  } else {
    // Create mode
    productStore.addProduct(selectedProduct)
    eventStore.popupMessage('success', 'Create Product successful!')
    router.push({ name: 'admin-products' })
  }
}
</script>

<template>
  <AdminLayout>
    <div class="flex pt-8 px-6">
      <div class="card w-full p-6 bg-base-100 shadow-xl mt-2">
        <div class="text-xl font-semibold">{{ mode }} Product</div>
        <div class="divider mt-2"></div>
        <div class="h-full w-full pb-6 bg-base-100">
          <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
            <div class="form-control w-full">
              <label class="label">
                <span class="label-text text-base-content">Name</span></label>
                <input
                type="text"
                placeholder=""
                class="input input-bordered w-full"
                v-model="selectedProduct.name"
              />
            </div>
            <div class="form-control w-full">
              <label class="label">
                <span class="label-text text-base-content">
                  Image
                </span>
              </label><input
                type="text"
                placeholder=""
                class="input input-bordered w-full"
                v-model="selectedProduct.imageUrl"
              />
            </div>
            <div class="form-control w-full">
              <label class="label">
                <span class="label-text text-base-content">
                  Price
                </span>
              </label>
              <input
                type="number"
                placeholder=""
                class="input input-bordered w-full"
                v-model="selectedProduct.price"
              />
            </div>
            <div class="form-control w-full">
              <label class="label">
                <span class="label-text text-base-content">
                  Quantity
                </span>
              </label>
              <input
                type="number"
                placeholder=""
                class="input input-bordered w-full"
                v-model="selectedProduct.quantity"
              />
            </div>
            <div class="form-control w-full">
              <label class="label">
                <span class="label-text text-base-content">
                  About
                </span>
              </label>
              <textarea
                class="textarea textarea-bordered w-full"
                v-model="selectedProduct.about"
                placeholder="detail product">
              </textarea>
            </div>
          </div>
          <div class="divider"></div>
          <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
            <div class="form-control w-full">
              <label class="label">
                <span class="label-text text-base-content">
                  Status
                </span>
              </label>
              <select class="select select-bordered w-full" v-model="selectedProduct.status">
                <option disabled selected>Status</option>
                <option value="open">Open</option>
                <option value="close">Close</option>
              </select>
            </div>
          </div>
          <div class="mt-4 flex justify-end">
            <RouterLink to="/admin/products" class="btn btn-ghost">
              Back
            </RouterLink>
            <button @click="updateProduct()" class="btn btn-primary ml-4">
              {{ mode }}
            </button>
          </div>
        </div>
      </div>
    </div>
  </AdminLayout>
</template>

ผลลัพธ์ตอนเปิด Form มา

product-02

ผลลัพธ์ตอนมาจากส่วนแก้ไข

product-03