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:
... <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:
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:
<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:
... <template> <div> <span style="font-size:30px;cursor:pointer" v-on:click="openNav">☰</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:
<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:
<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:
... 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 .
... 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 .
... 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
:
<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:
... <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:
<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:
... <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:
<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:
... <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">×</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ó.
... <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
:
... <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 .
... 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.
... 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, formData
và axios
đượ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 .
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:
<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.
... <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.
Các tin liên quan