Thứ năm, 12/12/2019 | 00:00 GMT+7

Cách tạo một ứng dụng lập hóa đơn đơn giản với node: Giao diện người dùng

Trong phần đầu tiên của loạt bài này, bạn cài đặt server backend cho ứng dụng lập hóa đơn. Trong hướng dẫn này, bạn sẽ xây dựng một phần của ứng dụng mà user sẽ tương tác, được gọi là giao diện user .

Yêu cầu

Để theo dõi bài viết này, bạn cần những thứ sau:

  • Nút được cài đặt trên máy của bạn
  • NPM được cài đặt trên máy của bạn
  • Đã đọc qua phần đầu tiên của loạt bài này.

Bước 1 - Cài đặt Vue

Để xây dựng giao diện user của ứng dụng này, bạn sẽ sử dụng Vue . Vue là một khung JavaScript tiến bộ được sử dụng để xây dựng các giao diện user hữu ích và tương tác. Để cài đặt Vue, hãy chạy lệnh sau:

  • npm install -g vue-cli

Để xác nhận cài đặt Vue của bạn, hãy chạy lệnh sau:

vue --version 

Số version sẽ được trả lại nếu Vue được cài đặt.

Bước 2 - Tạo dự án

Để tạo một dự án mới với Vue, hãy chạy lệnh sau và sau đó làm theo dấu nhắc :

  • vue init webpack invoicing-app-frontend

Điều này tạo ra một dự án Vue mẫu mà ta sẽ xây dựng trong bài viết này.

Đối với giao diện user của ứng dụng lập hóa đơn này, rất nhiều yêu cầu sẽ được gửi đến server backend . Để làm điều này, ta sẽ sử dụng axios . Để cài đặt axios , hãy chạy lệnh trong folder dự án của bạn:

  • npm install axios --save

Để cho phép một số kiểu mặc định trong ứng dụng, bạn sẽ sử dụng Bootstrap . Để thêm nó vào ứng dụng của bạn, hãy thêm phần sau vào index.html trong dự án:

index.html
    ...       <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">     ...         <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>         <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js" integrity="sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ" crossorigin="anonymous"></script>         <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js" integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm"crossorigin="anonymous"></script>     ... 

Bước 3 - Cấu hình bộ định tuyến Vue

Đối với ứng dụng này, bạn sẽ có hai lộ trình chính:

  • / để hiển thị trang đăng nhập
  • /dashboard để hiển thị trang tổng quan của user

Để cấu hình các tuyến này, hãy mở src/router/index.js và cập nhật nó trông giống như sau:

src / router / index.js
 import Vue from 'vue' import Router from 'vue-router' import SignUp from '@/components/SignUp' import Dashboard from '@/components/Dashboard' Vue.use(Router) export default new Router({   mode: 'history',   routes: [     {       path: '/',       name: 'SignUp',       component: SignUp     },     {       path: '/dashboard',       name: 'Dashboard',       component: Dashboard     },   ] }) 

Điều này chỉ định các thành phần sẽ được hiển thị cho user khi họ truy cập ứng dụng của bạn.

Bước 4 - Tạo thành phần

Một trong những điểm bán hàng chính của Vue là cấu trúc thành phần. Các thành phần cho phép giao diện user của ứng dụng của bạn trở nên module hơn và có thể tái sử dụng. Ứng dụng này sẽ có các thành phần sau:

  • Đăng kí đăng nhập
  • Tiêu đề
  • dẫn đường
  • console
  • Xem hóa đơn
  • Tạo hóa đơn
  • Một hóa đơn

Thành phần Điều hướng là thanh bên sẽ chứa các liên kết của các hành động khác nhau. Tạo một thành phần mới trong folder /src/components :

  • touch SideNav.vue

Bây giờ, hãy chỉnh sửa SideNav.vue như thế này:

src / components / SideNav.vue
 <script> export default {   name: "SideNav",   props: ["name", "company"],   methods: {    setActive(option) {       this.$parent.$parent.isactive = option;     },     openNav() {       document.getElementById("leftsidenav").style.width = "20%";     },     closeNav() {       document.getElementById("leftsidenav").style.width = "0%";     }   } }; </script>  ... 

Thành phần được tạo với hai props : tên của user và tên của công ty. Hai phương pháp này thêm chức năng thu gọn vào thanh bên. Phương thức setActive sẽ cập nhật thành phần gọi thành phần cha của thành phần SideNav , trong trường hợp này là Dashboard , khi user nhấp vào liên kết chuyển .

Thành phần có mẫu sau:

src / components / SideNav.vue
...   <template>     <div>         <span style="font-size:30px;cursor:pointer" v-on:click="openNav">&#9776;</span>         <div id="leftsidenav" class="sidenav">             <p style="font-size:12px;cursor:pointer" v-on:click="closeNav"><em>Close Nav</em></p>             <p><em>Company: {{ company }} </em></p>             <h3>Welcome, {{ name }}</h3>             <p class="clickable" v-on:click="setActive('create')">Create Invoice</p>             <p class="clickable" v-on:click="setActive('view')">View Invoices</p>         </div>     </div> </template> ... 

Thành phần Header hiển thị tên của ứng dụng và thanh bên nếu user đã đăng nhập. Tạo file Header.vue trong folder src/components :

  • touch Header.vue

Tệp thành phần sẽ giống như sau:

src / components / Header.vue
<template>     <nav class="navbar navbar-light bg-light">         <template v-if="user != null">             <SideNav v-bind:name="user.name" v-bind:company="user.company_name"/>         </template>         <span class="navbar-brand mb-0 h1">{{title}}</span>     </nav> </template> <script> import SideNav from './SideNav' export default {   name: "Header",   props : ["user"],   components: {     SideNav   },   data() {     return {       title: "Invoicing App",     };   } }; </script> 

Thành phần tiêu đề có một prop duy nhất được gọi là user . Phần prop này sẽ được thông qua bởi bất kỳ thành phần nào sẽ sử dụng thành phần tiêu đề. Trong mẫu cho tiêu đề, thành phần SideNav đã tạo trước đó được nhập và kết xuất có điều kiện được sử dụng để xác định xem SideNav có được hiển thị hay không.

Thành phần SignIn chứa biểu mẫu đăng ký và đăng nhập. Tạo một file mới trong folder /src/components :

  • touch SignIn.vue

Thành phần đăng ký và đăng nhập user phức tạp hơn một chút so với hai thành phần trước đó. Ứng dụng cần có biểu mẫu đăng nhập và đăng ký trên cùng một trang.

Đầu tiên, tạo thành phần:

src / components / SignIn.vue
<script> import Header from "./Header"; import axios from "axios"; export default {   name: "SignUp",   components: {     Header   },   data() {     return {       model: {         name: "",         email: "",         password: "",         c_password: "",         company_name: ""       },       loading: "",       status: ""     };   },   ... 

Thành phần Header được nhập và các thuộc tính dữ liệu của các thành phần cũng được chỉ định. Tiếp theo, tạo các phương pháp để xử lý những gì xảy ra khi dữ liệu được gửi:

src / components / SignIn.vue
  ...   methods: {     validate() {       // checks to ensure passwords match       if( this.model.password != this.model.c_password){         return false;       }       return true;     },     ... 

Phương thức validate() thực hiện kiểm tra đảm bảo dữ liệu do user gửi đáp ứng các yêu cầu của ta .

src / components / SignIn.vue
... register() { const formData = new FormData(); let valid = this.validate(); if(valid){ formData.append("name", this.model.name); formData.append("email", this.model.email); formData.append("company_name", this.model.company_name); formData.append("password", this.model.password);  this.loading = "Registering you, please wait"; // Post to server axios.post("http://localhost:3128/register", formData).then(res => {   // Post a status message   this.loading = "";   if (res.data.status == true) {     // now send the user to the next route     this.$router.push({       name: "Dashboard",       params: { user: res.data.user }     });   } else {     this.status = res.data.message;   } });   }else{ alert("Passwords do not match");   } }, ... 

Phương thức register của thành phần xử lý hành động khi user cố gắng đăng ký một account mới. Đầu tiên, dữ liệu được xác thực bằng phương pháp validate . Sau đó, nếu tất cả các tiêu chí được đáp ứng, dữ liệu sẽ được chuẩn bị để gửi bằng formData .

Ta cũng đã xác định thuộc tính loading của thành phần để cho user biết khi nào biểu mẫu của họ đang được xử lý. Cuối cùng, một yêu cầu POST được gửi đến server backend bằng cách sử dụng axios. Khi nhận được phản hồi từ server với trạng thái true , user sẽ được chuyển hướng đến trang tổng quan. Nếu không, một thông báo lỗi sẽ được hiển thị cho user .

src / components / SignIn.vue
  ...   login() {   const formData = new FormData();   formData.append("email", this.model.email);   formData.append("password", this.model.password);   this.loading = "Signing in";   // Post to server   axios.post("http://localhost:3128/login", formData).then(res => {     // Post a status message     this.loading = "";     if (res.data.status == true) {       // now send the user to the next route       this.$router.push({         name: "Dashboard",         params: { user: res.data.user }       });     } else {       this.status = res.data.message;     }   }); }   } }; </script> 

Cũng giống như phương pháp register , dữ liệu được chuẩn bị và gửi đến server backend để xác thực user . Nếu user tồn tại và thông tin chi tiết khớp, user sẽ được chuyển hướng đến trang tổng quan của họ.

Bây giờ, hãy xem mẫu cho thành phần SignUp :

src / components / SignUp.vue
   <template>    ...    <div class="tab-pane fade show active" id="pills-login" role="tabpanel" aria-labelledby="pills-login-tab">   <div class="row">   <div class="col-md-12">       <form @submit.prevent="login">           <div class="form-group">               <label for="">Email:</label>               <input type="email" required class="form-control" placeholder="eg chris@invoiceapp.com" v-model="model.email">           </div>            <div class="form-group">               <label for="">Password:</label>               <input type="password" required class="form-control" placeholder="Enter Password" v-model="model.password">           </div>            <div class="form-group">               <button class="btn btn-primary" >Login</button>               {{ loading }}               {{ status }}           </div>       </form>    </div>   </div>   </div>   ... 

Đăng nhập trong biểu mẫu được hiển thị ở trên và các trường đầu vào được liên kết với các thuộc tính dữ liệu tương ứng được chỉ định khi các thành phần được tạo. Khi nhấp vào nút gửi của biểu mẫu, phương thức login của thành phần được gọi.

Thông thường, khi nhấp vào nút gửi của biểu mẫu, biểu mẫu sẽ được gửi thông qua yêu cầu GET hoặc POST . Thay vì sử dụng điều đó, ta đã thêm <form @submit.prevent="login"> khi tạo biểu mẫu để overrides hành vi mặc định và chỉ định rằng hàm đăng nhập sẽ được gọi.

Biểu mẫu đăng ký cũng giống như thế này:

src / components / SignIn.vue
  ... <div class="tab-pane fade" id="pills-register" role="tabpanel" aria-labelledby="pills-register-tab">   <div class="row">   <div class="col-md-12">       <form @submit.prevent="register">           <div class="form-group">               <label for="">Name:</label>               <input type="text" required class="form-control" placeholder="eg Chris" v-model="model.name">           </div>           <div class="form-group">               <label for="">Email:</label>               <input type="email" required class="form-control" placeholder="eg chris@invoiceapp.com" v-model="model.email">           </div>            <div class="form-group">               <label for="">Company Name:</label>               <input type="text" required class="form-control" placeholder="eg Chris Tech" v-model="model.company_name">           </div>           <div class="form-group">               <label for="">Password:</label>               <input type="password" required class="form-control" placeholder="Enter Password" v-model="model.password">           </div>           <div class="form-group">               <label for="">Confirm Password:</label>               <input type="password" required class="form-control" placeholder="Confirm Passowrd" v-model="model.confirm_password">           </div>           <div class="form-group">               <button class="btn btn-primary" >Register</button>               {{ loading }}               {{ status }}           </div>       </form>   </div>   </div>   ... </template> 

@submit.prevent cũng được sử dụng ở đây để gọi phương thức register khi nút gửi được nhấp.

Bây giờ, hãy chạy server phát triển của bạn bằng lệnh này:

  • npm run dev

Truy cập localhost:8080/ trên trình duyệt của bạn để xem trang đăng nhập và đăng ký mới được tạo.

Thành phần Control panel sẽ được hiển thị khi user được chuyển đến tuyến /dashboard . Nó hiển thị Header và thành phần CreateInvoice theo mặc định. Tạo file Dashboard.vue trong folder src/components

  • touch Dashboard.vue

Chỉnh sửa file để trông giống như sau:

src / component / Dashboard.vue
<script> import Header from "./Header"; import CreateInvoice from "./CreateInvoice"; import ViewInvoices from "./ViewInvoices"; export default {   name: "Dashboard",   components: {     Header,     CreateInvoice,     ViewInvoices,   },   data() {     return {       isactive: 'create',       title: "Invoicing App",       user : (this.$route.params.user) ? this.$route.params.user : null     };   } }; </script> ... 

Ở trên, các thành phần cần thiết được nhập và hiển thị dựa trên mẫu bên dưới:

src / component / Dashboard.vue
... <template>   <div class="container-fluid" style="padding: 0px;">     <Header v-bind:user="user"/>     <template v-if="this.isactive == 'create'">       <CreateInvoice />     </template>     <template v-else>       <ViewInvoices />     </template>   </div> </template> 

Thành phần CreateInvoice chứa biểu mẫu cần thiết để tạo một hóa đơn mới. Tạo một file mới trong folder src/components :

  • touch CreateInvoice.vue

Chỉnh sửa thành phần CreateInvoice để trông giống như sau:

src / components / CreateInvoice.vue
 <template>   <div>     <div class="container">       <div class="tab-pane fade show active">         <div class="row">           <div class="col-md-12">             <h3>Enter Details below to Create Invoice</h3>             <form @submit.prevent="onSubmit">               <div class="form-group">                 <label for="">Invoice Name:</label>                   <input type="text" required class="form-control" placeholder="eg Seller's Invoice" v-model="invoice.name">               </div>               <div class="form-group">                 <label for="">Invoice Price:</label><span> $ {{ invoice.total_price }}</span>               </div>               ... 

Điều này tạo ra một biểu mẫu chấp nhận tên của hóa đơn và hiển thị tổng giá của hóa đơn. Tổng giá thu được bằng cách tổng hợp giá của các giao dịch riêng lẻ cho hóa đơn.

Hãy xem cách các giao dịch được thêm vào hóa đơn:

src / components / CreateInvoice.vue
... <hr /> <h3> Transactions </h3> <div class="form-group">   <label for="">Add Transaction:</label>   <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#transactionModal">     +   </button>   <!-- Modal -->   <div class="modal fade" id="transactionModal" tabindex="-1" role="dialog" aria-labelledby="transactionModalLabel" aria-hidden="true">     <div class="modal-dialog" role="document">       <div class="modal-content">         <div class="modal-header">           <h5 class="modal-title" id="exampleModalLabel">Add Transaction</h5>           <button type="button" class="close" data-dismiss="modal" aria-label="Close">             <span aria-hidden="true">&times;</span>           </button>         </div>         <div class="modal-body">           <div class="form-group">             <label for="">Transaction name</label>             <input type="text" id="txn_name_modal" class="form-control">           </div>           <div class="form-group">             <label for="">Price ($)</label>             <input type="numeric" id="txn_price_modal" class="form-control">           </div>         </div>         <div class="modal-footer">           <button type="button" class="btn btn-secondary" data-dismiss="modal">Discard Transaction</button>           <button type="button" class="btn btn-primary" data-dismiss="modal" v-on:click="saveTransaction()">Save transaction</button>         </div>       </div>     </div>   </div> </div> ... 

Một nút được hiển thị để user thêm giao dịch mới. Khi nút được nhấp, một phương thức được hiển thị cho user để nhập thông tin chi tiết của giao dịch. Khi nhấp vào nút Save Transaction , một phương thức sẽ thêm nó vào các giao dịch hiện có.

src / components / CreateInvoice.vue
... <div class="col-md-12">   <table class="table">     <thead>       <tr>         <th scope="col">#</th>         <th scope="col">Transaction Name</th>         <th scope="col">Price ($)</th>         <th scope="col"></th>       </tr>     </thead>     <tbody>       <template v-for="txn in transactions">         <tr :key="txn.id">           <th>{{ txn.id }}</th>           <td>{{ txn.name }}</td>           <td>{{ txn.price }} </td>           <td><button type="button" class="btn btn-danger" v-on:click="deleteTransaction(txn.id)">X</button></td>         </tr>       </template>     </tbody>   </table> </div>    <div class="form-group">     <button class="btn btn-primary" >Create Invoice</button>     {{ loading }}     {{ status }}   </div> </form>    </div> </div>   </div> </div>   </div> </template> ... 

Các giao dịch hiện tại được hiển thị dưới dạng bảng. Khi nhấp vào nút X , giao dịch được đề cập sẽ bị xóa khỏi danh sách giao dịch và nvoice Price được tính lại. Cuối cùng, nút Create Invoice kích hoạt một chức năng sau đó chuẩn bị dữ liệu và gửi dữ liệu đó đến server backend để tạo hóa đơn.

Ta hãy cũng xem xét cấu trúc thành phần của thành phần Create Invoice :

src / components / CreateInvoice.vue
... <script> import axios from "axios"; export default {   name: "CreateInvoice",   data() {     return {       invoice: {         name: "",         total_price: 0       },       transactions: [],       nextTxnId: 1,       loading: "",       status: ""     };   },   ... 

Đầu tiên, bạn đã xác định các thuộc tính dữ liệu cho thành phần. Thành phần sẽ có một đối tượng hóa đơn chứa name hóa đơn và total_price . Nó cũng sẽ có một loạt các transactions với index nextTxnId . Điều này sẽ theo dõi các giao dịch và các biến để gửi cập nhật trạng thái cho user .

src / components / CreateInvoice.vue
  ...   methods: {     saveTransaction() {       // append data to the arrays       let name = document.getElementById("txn_name_modal").value;       let price = document.getElementById("txn_price_modal").value;        if( name.length != 0 && price > 0){         this.transactions.push({           id: this.nextTxnId,           name: name,           price: price         });         this.nextTxnId++;         this.calcTotal();         // clear their values         document.getElementById("txn_name_modal").value = "";         document.getElementById("txn_price_modal").value = "";       }     },     ... 

Các phương thức cho thành phần CreateInvoice cũng được xác định ở đây. Phương thức saveTransaction() nhận các giá trị trong phương thức biểu mẫu giao dịch và sau đó thêm nó vào danh sách giao dịch. Phương thức deleteTransaction() xóa đối tượng giao dịch hiện có khỏi danh sách giao dịch trong khi phương thức calcTotal() tính toán lại tổng giá hóa đơn khi một giao dịch mới được thêm vào hoặc xóa.

src / components / CreateInvoice.vue
    ... deleteTransaction(id) {   let newList = this.transactions.filter(function(el) {     return el.id !== id;   });   this.nextTxnId--;   this.transactions = newList;   this.calcTotal(); }, calcTotal(){   let total = 0;   this.transactions.forEach(element => {     total += parseInt(element.price);   });   this.invoice.total_price = total; }, ... 

Cuối cùng, phương thức onSubmit() sẽ gửi biểu mẫu đến server backend . Trong phương thức, formDataaxios được sử dụng để gửi các yêu cầu. Mảng giao dịch chứa các đối tượng giao dịch được chia thành hai mảng khác nhau. Một mảng giữ tên giao dịch và mảng kia giữ giá giao dịch. Sau đó, server sẽ cố gắng xử lý yêu cầu và gửi lại phản hồi cho user .

src / components / CreateInvoice.vue
onSubmit() {   const formData = new FormData();   // format for request   let txn_names = [];   let txn_prices = [];   this.transactions.forEach(element => {     txn_names.push(element.name);     txn_prices.push(element.price);   });   formData.append("name", this.invoice.name);   formData.append("txn_names", txn_names);   formData.append("txn_prices", txn_prices);   formData.append("user_id", this.$route.params.user.id);   this.loading = "Creating Invoice, please wait ...";   // Post to server   axios.post("http://localhost:3128/invoice", formData).then(res => {     // Post a status message     this.loading = "";     if (res.data.status == true) {       this.status = res.data.message;     } else {       this.status = res.data.message;     }   }); }   } }; </script> 

Khi bạn quay lại ứng dụng trên localhost:8080 và đăng nhập, bạn sẽ được chuyển hướng đến trang tổng quan.

Đến đây bạn có thể tạo hóa đơn, bước tiếp theo là tạo hình ảnh trực quan về hóa đơn và trạng thái của chúng. Để thực hiện việc này, hãy tạo file ViewInvoices.vue trong folder src/components của ứng dụng.

  • touch ViewInvoices.vue

Chỉnh sửa file để trông giống như sau:

src / components / ViewInvoices.vue
<template>   <div>     <div class="container">       <div class="tab-pane fade show active">         <div class="row">           <div class="col-md-12">             <h3>Here are a list of your Invoices</h3>             <table class="table">               <thead>                 <tr>                   <th scope="col">Invoice #</th>                   <th scope="col">Invoice Name</th>                   <th scope="col">Status</th>                   <th scope="col"></th>                 </tr>               </thead>               <tbody>                 <template v-for="invoice in invoices">                   <tr>                     <th scope="row">{{ invoice.id }}</th>                     <td>{{ invoice.name }}</td>                     <td v-if="invoice.paid == 0 "> Unpaid </td>                     <td v-else> Paid </td>                     <td ><a href="#" class="btn btn-success">TO INVOICE</a></td>                         </tr>                 </template>               </tbody>             </table>           </div>         </div>       </div>     </div>   </div> </template>     ... 

Mẫu trên chứa một bảng hiển thị các hóa đơn mà user đã tạo. Nó cũng có một nút đưa user đến một trang hóa đơn khi một hóa đơn được nhấp vào.

src / components / ViewInvoice.vue
... <script> import axios from "axios"; export default {   name: "ViewInvoices",   data() {     return {       invoices: [],       user: this.$route.params.user     };   },   mounted() {     axios       .get(`http://localhost:3128/invoice/user/${this.user.id}`)       .then(res => {         if (res.data.status == true) {           this.invoices = res.data.invoices;         }       });   } }; </script> 

Thành phần ViewInvoices có thuộc tính dữ liệu của nó như một mảng hóa đơn và chi tiết user . Các chi tiết user có được từ các tham số tuyến đường. Khi thành phần được mounted , một yêu cầu GET được thực hiện tới server backend để tìm nạp danh sách các hóa đơn do user tạo, sau đó được hiển thị bằng cách sử dụng mẫu được hiển thị trước đó.

Khi bạn đi tới Trang tổng quan, hãy nhấp vào tùy chọn Xem hóa đơn trên SideNav để xem danh sách các hóa đơn có trạng thái thanh toán.

Kết luận

Trong phần này của loạt bài, bạn đã cấu hình giao diện user của ứng dụng lập hóa đơn bằng các khái niệm từ Vue. Trong phần tiếp theo của loạt bài này, bạn sẽ xem xét cách thêm xác thực JWT để duy trì phiên user , xem các hóa đơn đơn lẻ và gửi hóa đơn qua email cho người nhận.


Tags:

Các tin liên quan