EduCore & FinFlow Workspace ตอนที่ 2: การปฏิวัติหน้าบ้านด้วย Vue 3.5 V2 Architecture, ระบบจัดการสถานะ และ UI ยุคใหม่

เจาะลึกส่วนติดต่อผู้ใช้ (Frontend) ของ EduCore & FinFlow Workspace ในการอัปเกรดเทคโนโลยีไปสู่ V2 Architecture ด้วย Vue 3.5 Composition API, TypeScript, Pinia, TanStack Query และการทำงานแบบสอดคล้องตามอุปกรณ์

· 9 min read

Problem

ระบบหน้าบ้านแบบเดิม (Legacy V1) ใช้ Options API ร่วมกับ Vuex และการสร้าง HTTP client ชั้นนอกโดยไม่มีสเปคประเภทข้อมูล (Type Safety) ส่งผลให้พบปัญหาการเรนเดอร์ UI ผิดพลาดเมื่อมีการเปลี่ยนแปลงข้อมูลโครงสร้างงบประมาณจากเซิร์ฟเวอร์ และมีความล่าช้าในการพัฒนาฟีเจอร์จัดซื้อ

Solution

ออกแบบและจัดวางโครงสร้างหน้าบ้านใหม่ภายใต้โฟลเดอร์ /src/v2 ทั้งหมด โดยการพัฒนาบนฐาน Vue 3.5 + TS ควบคุม Client-side State ด้วย Pinia และจัดการ Server-side State ด้วย TanStack Query เพื่อการทำ Caching ข้อมูล พร้อมใช้ PrimeVue 4 สร้างความสวยงาม

Impact

หน้าจอมีความลื่นไหลเป็นธรรมชาติ การเรียกข้อมูลลดลงกว่า 60% ผ่านกลไก Cache อัจฉริยะ โค้ดของทีมผ่านการตรวจจับข้อผิดพลาดระดับ Compile-time และรองรับการแสดงผลตารางงบประมาณแบบ Responsive Pagination 3/5/10 รายการ

1. บริบทการย้ายระบบและปรับปรุงส่วนติดต่อผู้ใช้ใน Phase 2

ในการเข้ามารับงานต่อใน Phase 2 สำหรับส่วนติดต่อผู้ใช้ (Frontend) ของระบบ EduCore & FinFlow Workspace ทีมงานเผชิญกับโครงสร้างหน้าบ้านเดิมที่เป็นระบบ Legacy V1:

  • โค้ดเขียนแบบ Options API (JavaScript): นำไปสู่การจัดวางสปาเกตตีโค้ดที่ขยายและทำความเข้าใจตรรกะได้ยากเมื่อไฟล์มีขนาดใหญ่ขึ้น
  • การจัดการข้อมูลผ่าน Vuex 4: ข้อมูลของแต่ละฟีเจอร์เกิดความทับซ้อนและมีโอกาสเกิดสภาวะแย่งชิงข้อมูล (Race Condition) ได้ง่ายเมื่อเปิดหลายแท็บหน้าจอ
  • ไม่มี Type Safety: ระบบมักจะทำงานล้มเหลว (Runtime Crashes) เมื่อข้อมูลตอบกลับจาก API ส่งค่าที่เป็น null หรือฟิลด์ที่ไม่ได้คาดการณ์ไว้

ในการอัปเกรดเพื่อรองรับความคงทนและประสิทธิภาพที่สูงขึ้น ทีมพัฒนาได้กำหนดและรังสรรค์ V2 Architecture ขึ้นใหม่ภายใต้พาธ /src/v2 โดยบังคับใช้ TypeScript, Vue 3.5 และคอมโพเนนต์การเรนเดอร์ประสิทธิภาพสูงจาก PrimeVue 4


2. โครงสร้างและเลเยอร์ของคอมโพเนนต์หน้าบ้าน (V2 Component Architecture)

เพื่อแยกหน้าที่การทำงานของโค้ดให้สั้นและกระชับ สถาปัตยกรรม V2 แบ่งแยกความรับผิดชอบออกเป็นชั้นย่อย:

graph LR
    subgraph UI_Layer [UI Presentation]
        View[View: BudgetEstimationList.vue]
        Comp[Dialog: BudgetEstimationDialog.vue]
    end

    subgraph State_Layer [Logic & Reactivity]
        FormComp[Form Composable: useBudgetEstimationForm]
        QueryComp[Query Composable: useBudgetEstimationQuery]
    end

    subgraph Storage_Network [Storage & API Services]
        PiniaStore[Client Store: Pinia]
        AxiosClient[Axios V2 client]
    end

    View --> FormComp
    View --> Comp
    Comp --> FormComp
    FormComp --> QueryComp
    FormComp --> PiniaStore
    QueryComp --> AxiosClient
    AxiosClient --> API_V2[Backend API /api/v2]

หน้าที่แต่ละส่วน

  • View (Page Owner): ดูแลเรื่อง Layout และการผูก Composables เข้ากับองค์ประกอบเมนูนำทางหลัก
  • Composables: แยกตรรกะการประมวลผล ฟอร์ม และ State การโหลดข้อมูลออกจากคอมโพเนนต์หน้าจอหลัก
  • Components: UI ชิ้นย่อยที่ทำงานและรับค่าผ่านพารามิเตอร์ Props และสื่อสารกลับขึ้นไปผ่านการ Emit Events เท่านั้น
  • Services / Client: จัดการติดต่อเครือข่ายและแนบ Token ตรวจสอบสิทธิ์กลางของสถาบัน (Centralized OAuth)

3. โครงสร้างโฟลเดอร์ของ Frontend V2

ซอร์สโค้ด V2 ทั้งหมดจะอยู่ในกลุ่มโฟลเดอร์ต่อไปนี้:

  • src/v2/views/ และ src/v2/components/ สำหรับหน้าจอหลักและชิ้นส่วนควบคุม
  • src/v2/composables/ บรรจุ Logic ต่างๆ ในลักษณะ Hooks
  • src/v2/services/ และ src/v2/stores/ สำหรับจัดการฝั่ง Network API และเก็บค่า Client State เช่น โทเค็นล็อกอิน
  • src/v2/types/ สำหรับเก็บอินเทอร์เฟซไทป์กลาง (TypeScript Interfaces)

4. เจาะลึกซอร์สโค้ดตัวอย่างโมดูลประมาณการงบประมาณ (Budget Estimation)

นี่คือโครงสร้างจริงของโมดูล ประมาณการงบประมาณรายจ่าย (Budget Estimation) ที่ประยุกต์ใช้ V2 Stack:

4.1 ประกาศโครงสร้างประเภทข้อมูล: types/budget.ts

ช่วยควบคุมสเปคโครงสร้างข้อมูลตั้งแต่ฝั่ง IDE:

export interface BudgetEstimation {
  id: string;
  year: number;
  projectName: string;
  amount: number;
  status: "draft" | "pending_approval" | "approved" | "rejected";
}

export interface CreateBudgetInput {
  year: number;
  projectName: string;
  amount: number;
}

4.2 ตรรกะควบคุมและผูกข้อมูล (Composable Layer)

ใช้ประโยชน์จาก TanStack Vue Query และตรวจสอบฟอร์ม (Validation) ก่อนยิงข้อมูลเข้าสู่ API:

import { ref } from "vue";
import { useMutation, useQueryClient } from "@tanstack/vue-query";
import { CreateBudgetInput } from "../types/budget";

const budgetService = {
  create: async (data: CreateBudgetInput) => {
    // แนบ JWT และ POST ไปยัง /api/v2/budget-estimation
    return { id: "mock-id", ...data, status: "draft" };
  },
};

export function useBudgetEstimationForm(onSuccessCallback?: () => void) {
  const queryClient = useQueryClient();
  const errors = ref<Record<string, string>>({});

  const mutation = useMutation({
    mutationFn: budgetService.create,
    onSuccess: () => {
      // เคลียร์ Cache เพื่อสั่งอัปเดตหน้ารายการทันที
      queryClient.invalidateQueries({ queryKey: ["budget-estimations"] });
      if (onSuccessCallback) onSuccessCallback();
    },
  });

  const validateAndSubmit = async (formData: CreateBudgetInput) => {
    errors.value = {};
    if (!formData.projectName || formData.projectName.length < 5) {
      errors.value.projectName = "กรอกชื่อโครงการให้ถูกต้อง";
      return;
    }
    await mutation.mutateAsync(formData);
  };

  return { errors, validateAndSubmit, isSubmitting: mutation.isPending };
}

4.3 คอมโพเนนต์แสดงผลและระบบ Responsive Pagination

ตารางรายการนำความสามารถของ PrimeVue 4 มาจัดกลุ่มตารางข้อมูล โดยมีการเฝ้าระวังขนาดของหน้าจอ (Resize Event Listener) เพื่อปรับจำนวนแถวข้อมูลที่ดึงขึ้นมาให้พอดีกับขนาดหน้าจอผู้ใช้งานโดยอัตโนมัติ:

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { useQuery } from "@tanstack/vue-query";
import DataTable from "primevue/datatable";
import Column from "primevue/column";

const currentPage = ref(1);
const limitPerPage = ref(10); // ค่าเริ่มต้นจอ Desktop

const updateLimitByScreenSize = () => {
  const width = window.innerWidth;
  if (width < 768)
    limitPerPage.value = 3; // จอมือถือแสดง 3 แถว
  else if (width < 1024)
    limitPerPage.value = 5; // จอแท็บเล็ตแสดง 5 แถว
  else limitPerPage.value = 10; // จอขนาดใหญ่แสดง 10 แถว
};

onMounted(() => {
  updateLimitByScreenSize();
  window.addEventListener("resize", updateLimitByScreenSize);
});
onUnmounted(() => window.removeEventListener("resize", updateLimitByScreenSize));

// คอนฟิกการคิวรีข้อมูลอัจฉริยะร่วมกับ TanStack Query
const { data, isLoading } = useQuery({
  queryKey: ["budget-estimations", currentPage, limitPerPage],
  queryFn: () =>
    fetch(`/api/v2/budget-estimation?page=${currentPage.value}&limit=${limitPerPage.value}`).then(
      (res) => res.json()
    ),
});
</script>

<template>
  <div class="card p-4">
    <DataTable :value="data?.data" :loading="isLoading">
      <Column field="year" header="ปีงบประมาณ" />
      <Column field="projectName" header="ชื่อโครงการ" />
      <Column field="amount" header="งบประมาณ (บาท)" />
    </DataTable>
    <div class="mt-2 text-sm text-gray-500">
      แสดงผล {{ limitPerPage }} แถวต่อหน้า (Responsive Pagination)
    </div>
  </div>
</template>

5. สรุปผลลัพธ์การย้ายสู่หน้าบ้านยุคใหม่ (Frontend V2)

การบูรณาการเทคโนโลยี V2 ในส่วนติดต่อผู้ใช้ส่งมอบผลลัพธ์ที่ดีขึ้นเป็นทวีคูณ:

  • หมดปัญหา Runtime Crash: การกรองข้อมูลด้วย TypeScript ในระดับ Compile-time และ Zod ในระดับฟอร์ม ป้องกันไม่ให้แอปพลิเคชันค้าง
  • Caching ประสิทธิภาพสูง: TanStack Query ลดการเรียกเน็ตเวิร์กซ้ำซ้อน
  • Responsive Layout: ตัวคัดกรอง Pagination อัตโนมัติช่วยให้การทำรายการผ่านโทรศัพท์มือถือลื่นไหลและดูได้ง่ายสอดคล้องตามอุปกรณ์