First Assignment - 832201227 - Zhenpeng Lai - Frontend and Backend Separation Contacts

832201227赖振鹏 2024-10-31 14:42:06

Contact Management System


Course for This Assignment2401_MU_SE_EE308
Student IDFZU:832201227 MU:22126619
Assignment RequirementsCreate a contact management system that features distinct front-end and back-end components.
Objectives of This AssignmentImprove self-directed learning and gain essential skills in software development.
Other ReferencesAli front-end development Specification (2021 version)

目录

  • Contact Management System
  • 1. Assignment Introduction
  • 1.1 Project Objectives and Requirements
  • 1.2 Git Repository Links and Coding Standards
  • 1.3 Cloud Server Access Link
  • 1.4 PSP Table
  • 2. Finished Product Showcase
  • 2.1 Add Contact with Duplicate Check
  • 2.2 Edit Contact
  • 2.3 Delete Contact
  • 2.4 Search Contact
  • 2.5 Filter by Group
  • 2.6 Favorite Contact
  • 2.7 Alphabetical Sorting and Navigation
  • 3. Design and Implementation Process
  • 3.1 Project Background
  • 3.2 Project Significance
  • 3.3 Design Concept and Technology Selection
  • 3.4 Implementation Process
  • 3.4.1 Frontend Implementation
  • 3.4.2 Backend Implementation
  • 3.4.3 Testing and Debugging
  • 3.5 Project Deployment
  • 4. Project Structure and Workflow
  • 5. Code Explanation
  • 5.1 Key Frontend Code
  • 5.1.1 ContactForm.vue
  • 5.1.2 App.vue
  • 5.1.3 ContactList.vue
  • 5.2 Key Backend Code
  • 5.2.1 server.js
  • 5.2.2 db.js
  • 5.2.3 contactsController.js
  • 5.2.4 contacts.js
  • 6. Personal Learning and Reflections
  • 6.1 Learning Journey
  • 6.2 Future Improvements

1. Assignment Introduction

1.1 Project Objectives and Requirements

The purpose of this project is to develop a contact management system with a fully separated frontend and backend, implementing basic operations such as adding, modifying, and deleting contacts. The project is also deployed on a cloud server to enable user access for efficient contact management. This project is both a comprehensive test of frontend and backend development skills and an exercise in understanding essential workflows and technology stacks in modern web development.

In this project, I utilized Vue.js for the frontend, Express.js for the backend, and MySQL as the database management system. The frontend displays a contact list and includes functionality for adding, editing, and deleting contacts, ensuring that these operations interact directly with the backend database without using cached data. The core aspect of the project is frontend-backend separation, allowing for independent development, debugging, and deployment by enabling API-based communication between the frontend and backend, which improves development efficiency and maintainability.

The project follows a frontend-backend separation approach, with Vue.js used on the frontend and Express.js on the backend, managed via GitHub, and developed in compliance with Alibaba’s coding standards. Here are the GitHub links for the project repositories:

The codestyle.md file in the root directory of each repository documents coding standards, referencing Alibaba’s guidelines, including formatting, naming conventions, and commenting requirements to ensure readability and consistency.

The project is successfully deployed on an Alibaba Cloud server, and users can access the system at the following link:http://8.149.131.168:8080/
Note:
Due to the Cloud Server performance issues, the backend may occasionally crash, causing the page to display a "Fail to connect ..." error. If you encounter this issue, please feel free to contact me at QQ: 2331509873. Thank you for your understanding and support!

1.4 PSP Table

To manage time effectively, I created a PSP (Personal Software Process) table before starting the project, allowing for tracking and planning of the project development.

PSP StageEstimated TimeActual Time
Requirements Analysis2 hours2 hours
System Design3 hours3 hours
Frontend Development8 hours10 hours
Backend Development6 hours8 hours
Testing & Debugging3 hours4 hours
Deployment & Release2 hours2 hours
Total24 hours29 hours

2. Finished Product Showcase

The following is the final page display of the project, showcasing the overall interface design and main feature layout of the system. The specific functional modules will be introduced in detail below.

img

2.1 Add Contact with Duplicate Check

The system allows users to add new contacts. After filling in fields such as name, phone number, email, and group information in the form, users can click the "Submit" button. The data is then sent from the frontend to the backend and stored in the MySQL database. The contact list automatically refreshes, displaying a success message notifying the user that the new contact was successfully added. This feedback message disappears after a few seconds for a seamless user experience. The new contact also immediately appears in the contact list for easy access and management.

img

In addition, when adding a contact, the system will check if there is already a contact with the same name or phone number in the address book. If a duplicate is found, the page will prompt the user that a similar contact already exists and ask if they would like to update the information. If the user clicks "OK," the system will update the contact's information.

img

2.2 Edit Contact

In the contact list, users can click the "Edit" button to modify contact details. The system retrieves the contact’s information from the database and pre-fills it in the form. After making changes and clicking save, the updated data is synchronized with the database. A feedback message then appears on the page to notify the user that the changes have been successfully saved, and the contact list automatically refreshes to display the latest information. The feedback message fades after a few seconds, keeping the user interface clean and concise.

img

2.3 Delete Contact

When the "Delete" button is clicked, a confirmation prompt appears to prevent accidental deletions. Once confirmed, the system sends a request to the backend to delete the contact from the database. The contact list then refreshes to reflect the updated data.

img

2.4 Search Contact

Users can quickly find contacts by entering keywords in the search bar. The frontend sends a query request to the backend, which returns a list of matching contacts, allowing users to quickly locate specific contacts.

img

2.5 Filter by Group

The system supports filtering contacts by group (e.g., "Family," "Friends," "Work"). When a user selects a group, the contact list dynamically updates to display only contacts in the selected group, making it easier for users to manage contacts by category.

img

2.6 Favorite Contact

Users can mark important contacts by clicking the "Favorite" button. The favorite status is synchronized with the database, and users can filter the view to show only favorite contacts, allowing for quicker access to important contacts.

img

2.7 Alphabetical Sorting and Navigation

The system can sort contacts by the first letter of their names, making it easier for users to browse through large contact lists.

img

The alphabetical navigation feature enables users to jump directly to contacts based on the first letter of their name, making it easier to browse through large contact lists. This feature is especially useful when dealing with a substantial number of contacts.

img

3. Design and Implementation Process

3.1 Project Background

The background of this project stems from the need for effective contact management in daily life. As the number of contacts increases, it becomes essential to efficiently manage, categorize, and locate contact information. To address this, this project designed and implemented a contact management system based on frontend-backend separation. The system provides basic CRUD (Create, Read, Update, Delete) operations and introduces additional features like favoriting, group filtering, and alphabetical navigation to enhance the user experience and improve management efficiency.

3.2 Project Significance

This project is significant in several ways. First, it adopts a frontend-backend separation architecture, allowing for more flexible development by decoupling the frontend user interface from backend data management, making maintenance and future expansion easier. Additionally, deploying the project to the cloud enables data synchronization across multiple devices, overcoming the device dependency found in traditional contact management systems. Through this development, I have strengthened my technical skills with the Vue.js framework on the frontend and the Express.js framework on the backend, while gaining hands-on experience in database operations and API design. These skills are beneficial not only for this project but also lay a solid foundation for future development projects.

3.3 Design Concept and Technology Selection

The initial design goals of the project were simplicity, user-friendliness, and feature richness. To achieve these, we carefully selected frameworks and technologies for both the frontend and backend:

  • Frontend: Vue.js was chosen as the frontend framework. Vue.js’s reactive data binding capabilities are ideal for building highly interactive user interfaces. In this project, the frontend primarily handles the display and interaction of the user interface, including the contact list, adding contacts, editing contacts, group filtering, favoriting, and alphabetical navigation. Component-based development was utilized, packaging each feature into independent components to increase code reusability and maintainability.

  • Backend: Express.js was used to build a RESTful API, providing data management and business logic support. Express.js is a lightweight, flexible framework suitable for efficient API development. In this project, it was used to implement CRUD operations for contact data. The backend interacts with a MySQL database, performing data storage and updates via SQL queries.

  • Database: MySQL was selected as the data storage solution. MySQL’s structured data management allows for faster data querying and filtering, meeting the project’s requirements for contact data storage. Each contact record includes fields such as name, phone, email, group, and favorite status.

3.4 Implementation Process

3.4.1 Frontend Implementation

The frontend of this project is built using the Vue.js framework with a component-based architecture to manage contact functionalities. The core components include App.vue, ContactList.vue, and ContactForm.vue, with data exchanges facilitated through Axios:

  • App.vue – Application Controller
    App.vue is the main application component responsible for coordinating data exchange and state management between the frontend and backend. It loads and stores the contact list data, maintaining it as a global state that is passed down to child components. It centrally manages data flow and event handling, ensuring that the contact list remains up-to-date following each operation.

  • ContactList.vue – Contact List Display and Management
    This component displays all contact information and provides options for editing, deleting, and favoriting contacts. Users can manage contacts directly within the list by clicking buttons to edit contact details, delete contacts, or mark contacts as favorites. Additionally, the component supports alphabetical navigation, group filtering, and keyword search, making it easy for users to quickly find or categorize contacts.

  • ContactForm.vue – Contact Information Input and Editing
    Responsible for inputting and editing contact details, this form includes fields for name, phone number, email, and group selection. Basic data validation ensures that the input format is correct. When adding a new contact, the system checks for duplicates to avoid redundant data. Once a contact is successfully added or updated, a feedback message appears to notify the user that the action was completed.

  • Data Interaction: The frontend uses Axios to send HTTP requests to the backend, handling CRUD operations for contact data. All operation results are instantly reflected in the interface, allowing users to view the latest contact information.

3.4.2 Backend Implementation

The backend, built on the Express.js framework, is responsible for implementing the RESTful API, managing data, and handling errors:

  • RESTful API Endpoints
    The backend uses Express.js to create a RESTful API that covers CRUD operations, with each function having a corresponding route and controller method, including:

    • Add Contact: POST /api/contacts – Receives contact information and inserts it into the database.
    • Edit Contact: PUT /api/contacts/:id – Updates existing contact information based on contact ID.
    • Delete Contact: DELETE /api/contacts/:id – Deletes a contact from the database by ID.
    • Get Contact List: GET /api/contacts – Retrieves all contact data from the database and returns it to the frontend.
    • Toggle Favorite Status: PUT /api/contacts/:id/favorite – Toggles the favorite status based on contact ID.
  • Database Operations
    The backend uses a MySQL database to store contact information. The database schema includes fields such as name, phone, email, group, and favorite. The backend API performs SQL-based CRUD operations to ensure data reliability and consistency.

  • Error Handling
    The backend includes a detailed error handling mechanism that considers issues like data format errors and database connection failures. Appropriate status codes are returned, allowing the frontend to display user-friendly error messages.

3.4.3 Testing and Debugging

During development, I used Postman to test the API endpoints, ensuring that each endpoint returned the expected results under both normal and error conditions. Additionally, the frontend was manually tested to verify that each feature functioned as expected, providing a stable user experience.

3.5 Project Deployment

The project is deployed on Alibaba Cloud, enabling online access to the contact management system. The deployment steps are as follows:

  1. Environment Setup: Install Node.js and MySQL on the server, and configure Nginx as a reverse proxy to allow access via a domain name.

  2. Backend Deployment: Use pm2 to start the Express application on the server, ensuring that the backend service remains active even if the connection is interrupted.

  3. Frontend Deployment: Compile and bundle the frontend code using Vue CLI. The generated static files are then stored on the server and served by Nginx.

These steps enable users to access the system on the cloud server, providing full online management functionality for contacts.

4. Project Structure and Workflow

The overall project structure comprises the frontend and backend, primarily supporting CRUD operations for contact management. The frontend, built with Vue.js, follows a component-based design, allowing users to view the contact list and perform operations such as adding, editing, deleting, and favoriting contacts through the interface. When users initiate any operation on the interface, the frontend sends a request to the backend Express.js server via Axios. The backend processes the request, executes the relevant business logic, and performs CRUD operations with the MySQL database. The database is responsible for storing contact information, ensuring data consistency and reliability. Once the backend completes processing, it sends the result back to the frontend, which updates the UI to reflect the latest contact data, providing a seamless user experience.

This workflow, as illustrated in the following diagram, demonstrates an efficient frontend-backend interaction achieved through a layered architecture and modular design, making the system more scalable and maintainable.

img

5. Code Explanation

5.1 Key Frontend Code

5.1.1 ContactForm.vue

<template>
  <div>
    <h2>{{ editingContact ? "Edit Contact" : "Add Contact" }}</h2>
    <form @submit.prevent="submitForm">
      <!-- 输入表单行 -->
      <div class="form-row">
        <input v-model="name" type="text" placeholder="Name" required />
        <input v-model="phone" type="text" placeholder="Phone" required />
      </div>
      <div class="form-row">
        <input v-model="email" type="email" placeholder="Email" />
        <select v-model="group" required>
          <option disabled value="">Select Group</option>
          <option value="Family">Family</option>
          <option value="Friends">Friends</option>
          <option value="Work">Work</option>
          <option value="Other">Other</option>
        </select>
      </div>
      <!-- 收藏联系人复选框 -->
      <label>
        Favorite:
        <input type="checkbox" v-model="favorite" />
      </label>
      <!-- 提交按钮 -->
      <button type="submit">
        {{ editingContact ? "Update Contact" : "Add Contact" }}
      </button>

      <!-- 显示反馈信息 -->
      <transition name="fade" @after-leave="clearFeedbackMessage">
        <div v-if="feedbackMessage" class="feedback">
          {{ feedbackMessage }}
        </div>
      </transition>
    </form>
  </div>
</template>

<script>
import axios from "axios";

export default {
  props: {
    // 编辑模式下传递的联系人数据
    editingContact: Object,
    // 父组件传递的联系人列表
    contacts: {
      type: Array,
      default: () => [],  // 默认空数组
    },
  },
  data() {
    return {
      // 表单数据,若处于编辑模式则预填充数据
      name: this.editingContact ? this.editingContact.name : "",
      phone: this.editingContact ? this.editingContact.phone : "",
      email: this.editingContact ? this.editingContact.email : "",
      group: this.editingContact ? this.editingContact.group : "",
      favorite: this.editingContact ? !!this.editingContact.favorite : false,
      feedbackMessage: "", // 用户操作反馈消息
    };
  },
  methods: {
    /**
     * 提交表单,根据编辑模式调用不同的方法
     * @returns {void}
     */
    submitForm() {
        if (this.editingContact) {
            this.updateContact(this.editingContact); // 编辑模式,更新联系人
        } else {
            this.checkDuplicateContact(); // 新增模式,检查是否有重复联系人
        }
    },

    /**
     * 检查是否存在重复联系人
     * @returns {void}
     */
    checkDuplicateContact() {
        const newContact = {
            name: this.name.trim(),  // 去除多余空格
            phone: this.phone.trim(),
            email: this.email,
            group: this.group,
            favorite: this.favorite ? 1 : 0,
        };

        // 调用后端API进行重复检查
        axios.post('http://localhost:3000/api/contacts/check-duplicate', newContact)
            .then((response) => {
                const { exists, existingContact } = response.data;

                if (exists) {
                    if (confirm("A contact with the same name or phone exists. Update?")) {
                        this.updateContact(existingContact); // 更新重复联系人
                    } else {
                        this.resetForm(); // 重置表单
                    }
                } else {
                    this.addContact(); // 无重复,直接添加新联系人
                }
            })
            .catch((error) => {
                console.error("Error checking for duplicate contact:", error);
            });
    },

    /**
     * 更新现有联系人
     * @param {Object} existingContact - 要更新的联系人对象
     * @returns {void}
     */
    updateContact(existingContact) {
        if (!existingContact || !existingContact.id) {
            console.error("Invalid contact data");
            return;
        }

        const updatedContact = {
            ...existingContact,
            name: this.name,
            phone: this.phone,
            email: this.email,
            group: this.group,
            favorite: this.favorite ? 1 : 0,
        };

        // 调用后端API更新联系人
        axios.put(`http://localhost:3000/api/contacts/${existingContact.id}`, updatedContact)
            .then(() => {
                this.feedbackMessage = "Contact updated successfully!";
                this.resetForm(); // 重置表单
                this.$emit("contact-updated"); // 通知父组件更新联系人列表
                setTimeout(() => {
                    this.feedbackMessage = "";
                }, 3000);
            })
            .catch((error) => {
                console.error("Error updating contact:", error);
            });
    },

    /**
     * 添加新联系人
     * @returns {void}
     */
    addContact() {
        const newContact = {
            name: this.name.trim(),
            phone: this.phone.trim(),
            email: this.email,
            group: this.group,
            favorite: this.favorite ? 1 : 0,
        };

        // 调用后端API保存联系人
        axios.post("http://localhost:3000/api/contacts", newContact)
            .then(() => {
                this.feedbackMessage = "Contact added successfully!";
                this.resetForm(); // 重置表单
                this.$emit("contact-added"); // 通知父组件更新联系人列表
                setTimeout(() => {
                    this.feedbackMessage = "";
                }, 3000);
            })
            .catch((error) => {
                console.error("Error adding contact:", error);
            });
    },

    /**
     * 重置表单数据
     * @returns {void}
     */
    resetForm() {
        this.name = "";
        this.phone = "";
        this.email = "";
        this.group = "";
        this.favorite = false;
        this.$emit("reset-editing"); // 通知父组件重置编辑状态
    },

    /**
     * 清除反馈信息
     * @returns {void}
     */
    clearFeedbackMessage() {
        this.feedbackMessage = "";
    },
  },

  watch: {
    /**
     * 监控 editingContact 属性变化,动态更新表单数据
     */
    editingContact: {
      immediate: true,
      handler(newValue) {
        if (newValue) {
          // 编辑模式时填充表单数据
          this.name = newValue.name;
          this.phone = newValue.phone;
          this.email = newValue.email;
          this.group = newValue.group;
          this.favorite = !!newValue.favorite;
        } else {
          this.resetForm(); // 清空表单
        }
      },
    },
  },
};
</script>

<style scoped>
/* 表单过渡效果 */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}

/* 表单行样式 */
.form-row {
  display: flex;
  justify-content: space-between;
  width: 100%;
  margin-bottom: 10px;
  box-sizing: border-box;
}

/* 输入框和选择框样式 */
.form-row input,
.form-row select {
  width: 48%;
  padding: 8px;
  font-size: 1rem;
  box-sizing: border-box;
}

.form-row input:not(:last-child),
.form-row select:not(:last-child) {
  margin-right: 4%;
}

/* 提交按钮样式 */
button {
  padding: 10px 15px;
  background-color: #42b983;
  color: white;
  border: none;
  cursor: pointer;
}

button:hover {
  background-color: #3a9c7f;
}

form {
  width: 100%;
  box-sizing: border-box;
}

label {
  margin: 10px 0;
  display: block;
}

/* 反馈信息样式 */
.feedback {
  margin-top: 10px;
  color: green;
  font-weight: bold;
  transition: opacity 0.5s ease;
}
</style>

Purpose: This component is responsible for adding new contacts and editing existing ones. It handles form submission, validation, and communicates with the backend API to perform create and update operations.

Key Features:

  • Form Fields: The form includes input fields for name, phone, email, group selection, and a checkbox for marking a contact as a favorite.

  • Data Binding: Uses v-model to bind form inputs to component data properties, allowing for two-way data binding.

  • Form Submission:

    • Adding a Contact: When not in editing mode, the form checks for duplicate contacts before adding a new one.
    • Editing a Contact: When in editing mode, the form updates the existing contact's information.
  • Methods:

    • submitForm(): Determines whether to add a new contact or update an existing one based on the editing mode.
    • checkDuplicateContact(): Sends a request to the backend to check if a contact with the same name or phone number already exists.
    • addContact(): Sends a POST request to add a new contact to the backend.
    • updateContact(existingContact): Sends a PUT request to update an existing contact's information.
    • resetForm(): Clears the form fields and resets the editing state.
  • Feedback Mechanism: Provides user feedback messages upon successful addition or update of a contact, using transition effects for a smooth UI experience.

Explanation:

The ContactForm.vue component encapsulates the logic for both adding and editing contacts. It interacts with the backend API to perform necessary CRUD operations. By checking for duplicate contacts before adding a new one, it ensures data integrity. The component also emits events to inform parent components about updates, facilitating state management across the application.


5.1.2 App.vue

<template>
  <div id="app">
    <h1 class="app-title">Welcome to Address Book</h1>
    <!-- 联系人表单组件 -->
    <div class="form-container">
      <ContactForm
        :contacts="contacts"
        @contact-added="fetchContacts"
        @contact-updated="fetchContacts"
        @reset-editing="resetEditing"
        :editingContact="selectedContact"
      />
    </div>
    <!-- 联系人列表组件 -->
    <div class="contacts-list-container">
      <h2 class="contacts-list-title">Contacts List</h2>
      <ContactList
        :contacts="contacts"
        @edit-contact="editContact"
        @contact-deleted="fetchContacts"
      />
    </div>
  </div>
</template>

<script>
import ContactForm from './components/ContactForm.vue';
import ContactList from './components/ContactList.vue';
import axios from 'axios';

export default {
  data() {
    return {
      contacts: [],  // 存储联系人列表
      selectedContact: null,  // 当前选中的联系人,用于编辑
    };
  },
  components: {
    ContactForm,
    ContactList,
  },
  methods: {
    /**
     * @description 从后端获取最新的联系人列表
     * @returns {void}
     */
    fetchContacts() {
      axios
        .get('http://localhost:3000/api/contacts')
        .then((response) => {
          // 成功获取联系人数据,将其存入组件的状态中
          this.contacts = response.data;
        })
        .catch((error) => {
          // 捕获错误并输出到控制台
          console.error('Error fetching contacts:', error);
          // 提示用户请求失败的消息
          alert('Failed to fetch contacts. Please try again later.');
        });
    },
    /**
     * @description 选择一个联系人进行编辑
     * @param {Object} contact - 用户选择的联系人
     * @returns {void}
     */
    editContact(contact) {
      axios
        .get(`http://localhost:3000/api/contacts/${contact.id}`)
        .then((response) => {
          // 更新选中的联系人信息,确保从后端获取最新数据
          this.selectedContact = response.data;
        })
        .catch((error) => {
          console.error('Error fetching contact details:', error);
          alert('Failed to load contact details. Please try again later.');
        });
    },
    /**
     * @description 重置编辑状态
     * @returns {void}
     */
    resetEditing() {
      this.selectedContact = null;
    },
  },
  /**
   * @description 组件挂载后,立即从后端获取联系人列表
   * @returns {void}
   */
  mounted() {
    this.fetchContacts();
  },
};
</script>

<style scoped>
html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  overflow: hidden;
}

#app {
  min-height: 100%;
  overflow-y: auto;
  padding: 20px;
  box-sizing: border-box;
}

body {
  background-color: #f4f4f9;
  font-family: 'Arial', sans-serif;
}

.app-title {
  text-align: center;
  font-size: 2.5em;
  margin: 20px 0;
  font-weight: bold;
  color: #333;
}

.form-container {
  max-width: 600px;
  margin: 20px auto;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 10px;
  background-color: white;
}

.contacts-list-title {
  font-size: 1.8em;
  text-align: center;
  margin: 20px 0;
  font-weight: bold;
  color: #333;
}

.contacts-list-container {
  max-width: 600px;
  margin: 20px auto;
  padding: 20px;
  background-color: white;
  border: 1px solid #ddd;
  border-radius: 10px;
  overflow-y: auto;
}
</style>

Purpose: Serves as the main component of the application, orchestrating the interaction between the contact form and the contact list components. It manages the overall state of the contact data.

Key Features:

  • Data Management: Maintains the list of contacts and the currently selected contact for editing.

  • Components:

    • Includes ContactForm and ContactList components.
  • Methods:

    • fetchContacts(): Retrieves the latest list of contacts from the backend API.
    • editContact(contact): Sets a contact as the selected contact for editing.
    • resetEditing(): Resets the editing state when editing is complete.
  • Lifecycle Hook:

    • mounted(): Calls fetchContacts() to load the contact list when the component is mounted.

Explanation:

App.vue acts as the central hub of the application, coordinating data flow between the contact form and the contact list. It fetches the initial data from the backend and updates the contact list whenever changes occur. By managing the selected contact, it enables the editing functionality within the ContactForm component.


5.1.3 ContactList.vue

<template>
  <div>
    <!-- 搜索和筛选区 -->
    <div class="search-container">
      <!-- 搜索输入框 -->
      <input
        v-model="searchQuery"
        placeholder="Search contacts"
        class="search-input"
      />
      <!-- 仅显示收藏联系人 -->
      <label class="favorites-label">
        <input type="checkbox" v-model="showFavoritesOnly" /> Show Favorites
      </label>
      <!-- 分组选择下拉框 -->
      <select v-model="selectedGroup" class="group-select">
        <option value="">All Groups</option>
        <option value="Family">Family</option>
        <option value="Friends">Friends</option>
        <option value="Work">Work</option>
        <option value="Other">Other</option>
      </select>
    </div>

    <!-- 联系人列表 -->
    <div class="contact-list-container">
      <ul class="contact-list">
        <!-- 循环生成联系人项 -->
        <li
          v-for="contact in filteredContacts"
          :key="contact.id"
          :id="`contact-${getFirstLetter(contact.name)}`"
          class="contact-item"
        >
          <!-- 联系人信息展示 -->
          <div class="contact-info">
            <div class="name-phone">{{ contact.name }} - {{ contact.phone }}</div>
            <div>Group: {{ contact.group }}</div>
            <div>Email: {{ contact.email || '-' }}</div>
          </div>
          <!-- 联系人操作按钮 -->
          <div class="contact-actions">
            <button @click="editContact(contact)">Edit</button>
            <button @click="deleteContact(contact.id)">Delete</button>
            <button @click="toggleFavorite(contact)">
              {{ contact.favorite ? "Unfavorite" : "Favorite" }}
            </button>
          </div>
        </li>
      </ul>

      <!-- 字母导航栏 -->
      <div class="alphabet-nav">
        <!-- 循环生成字母导航 -->
        <div
          v-for="letter in uniqueFirstLetters"
          :key="letter"
          @click="scrollToLetter(letter)"
          class="alphabet-letter"
        >
          {{ letter }}
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import axios from 'axios';
import pinyin from 'pinyin';

export default {
  props: {
    contacts: Array, // 父组件传递的联系人数组
  },
  data() {
    return {
      searchQuery: '', // 搜索框输入值
      showFavoritesOnly: false, // 是否只显示收藏联系人
      selectedGroup: '', // 当前选择的分组
    };
  },
  computed: {
    /**
     * @description 生成联系人名字的首字母集合,用于字母导航栏
     * @returns {Array} 联系人名字首字母的数组
     */
    uniqueFirstLetters() {
      return [...new Set( // 使用 Set 去重
        this.filteredContacts
          .map((contact) => this.getFirstLetter(contact.name)) // 获取每个联系人的首字母
      )].sort(); // 排序
    },

    /**
     * @description 根据搜索条件、分组和收藏状态过滤联系人列表,并按首字母排序
     * @returns {Array} 过滤后的联系人数组
     */
    filteredContacts() {
      return this.contacts
        .filter((contact) => {
          // 根据名字或电话号码匹配搜索条件
          const matchesSearch =
            contact.name.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
            contact.phone.includes(this.searchQuery);

          // 只显示收藏联系人时,过滤非收藏联系人
          const matchesFavorite = !this.showFavoritesOnly || contact.favorite === 1;

          // 根据选择的分组进行筛选
          const matchesGroup = !this.selectedGroup || contact.group === this.selectedGroup;

          return matchesSearch && matchesFavorite && matchesGroup;
        })
        .sort((a, b) => {
          const nameA = this.getFirstLetter(a.name); // 获取A的首字母
          const nameB = this.getFirstLetter(b.name); // 获取B的首字母
          return nameA.localeCompare(nameB); // 按首字母排序
        });
    },
  },
  methods: {
    /**
     * @description 获取联系人的名字首字母
     * @param {String} name - 联系人名字
     * @returns {String} 联系人的首字母
     */
    getFirstLetter(name) {
      const trimmedName = name.trim(); // 去除多余的空格

      // 如果名字是英文字符,返回首字母并转为大写
      if (/^[A-Za-z]+$/.test(trimmedName)) {
        return trimmedName.charAt(0).toUpperCase();
      } else {
        // 处理中文名字的情况,返回拼音的首字母
        const pinyinArray = pinyin(trimmedName, { style: pinyin.STYLE_FIRST_LETTER });
        return pinyinArray[0][0].toUpperCase();
      }
    },

    /**
     * @description 触发编辑联系人事件,通知父组件
     * @param {Object} contact - 联系人对象
     */
    editContact(contact) {
      this.$emit('edit-contact', contact); // 向父组件发送编辑事件
    },

    /**
     * @description 删除联系人
     * @param {Number} contactId - 联系人ID
     */
    deleteContact(contactId) {
      if (confirm('Are you sure you want to delete this contact?')) {
        axios
          .delete(`http://localhost:3000/api/contacts/${contactId}`) // 调用API删除联系人
          .then(() => {
            this.$emit('contact-deleted'); // 向父组件发送删除成功的事件
          })
          .catch((error) => {
            console.error('Error deleting contact:', error);
          });
      }
    },

    /**
     * @description 切换联系人收藏状态
     * @param {Object} contact - 联系人对象
     */
    toggleFavorite(contact) {
      const updatedContact = {
        name: contact.name,
        phone: contact.phone,
        email: contact.email || '', // 如果没有 email,使用空字符串
        group: contact.group,
        favorite: contact.favorite === 1 ? 0 : 1, // 切换收藏状态
      };

      axios
        .put(`http://localhost:3000/api/contacts/${contact.id}`, updatedContact)
        .then(() => {
          contact.favorite = updatedContact.favorite;
          this.$forceUpdate(); // 强制更新组件
        })
        .catch((error) => {
          console.error('Error updating favorite:', error);
        });
    },

    /**
     * @description 根据字母滚动到相应的联系人
     * @param {String} letter - 字母
     */
    scrollToLetter(letter) {
      const element = document.getElementById(`contact-${letter}`);
      if (element) {
        element.scrollIntoView({ behavior: 'smooth', block: 'start' }); // 滚动到目标联系人
      }
    },
  },
};
</script>

<style scoped>
.search-container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
}

.search-input {
  flex-grow: 2;
  margin-right: 10px;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.favorites-label {
  flex-grow: 1;
  margin-right: 10px;
}

.group-select {
  flex-grow: 1;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.contact-list-container {
  position: relative;
  max-width: 600px;
  margin: 20px auto;
  padding: 20px;
  background-color: white;
  border: 1px solid #ddd;
  border-radius: 10px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  max-height: 600px;
  overflow-y: auto;
  padding-right: 30px;
  overflow-x: hidden;
}

.contact-list {
  list-style-type: none;
  padding: 0;
  margin: 0;
}

.contact-item {
  display: flex;
  justify-content: space-between;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 5px;
  margin-bottom: 5px;
  transition: background-color 0.3s;
  align-items: center;
}

.contact-item:hover {
  background-color: #f0f0f0;
}

.contact-info {
  flex: 1;
}

.contact-actions {
  display: flex;
  gap: 10px;
}

.contact-actions button {
  padding: 5px 10px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-size: 14px;
  min-width: 80px;
}

.contact-actions button:hover {
  background-color: #3a9c7f;
}

button:active {
  transform: scale(0.95);
}

.alphabet-nav {
  position: absolute;
  right: 0;
  top: 0;
  display: flex;
  flex-direction: column;
  padding: 3px;
  z-index: 9999;
  background-color: #fff;
  border-radius: 4px;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

.alphabet-letter {
  cursor: pointer;
  padding: 2px 4px;
  font-weight: bold;
  color: #333;
  text-align: center;
  font-size: 12px;
  margin-bottom: 2px;
}

.alphabet-letter:hover {
  background-color: #f0f0f0;
  color: #42b983;
  border-radius: 2px;
}
</style>

Purpose: Displays the list of contacts with functionalities for searching, filtering, and navigating contacts. It also allows users to edit, delete, and toggle the favorite status of contacts.

Key Features:

  • Search and Filtering:

    • Allows users to search contacts by name or phone number.
    • Filters contacts by group and favorite status.
  • Alphabetical Navigation:

    • Generates an alphabet navigation bar based on the first letters of contact names.
    • Provides quick scrolling to contacts starting with a specific letter.
  • Contact Actions:

    • Edit: Emits an event to the parent component to edit a contact.
    • Delete: Sends a DELETE request to the backend to remove a contact.
    • Favorite/Unfavorite: Toggles the favorite status of a contact by updating it in the backend.
  • Computed Properties:

    • uniqueFirstLetters: Computes a sorted list of unique first letters from contact names for the navigation bar.
    • filteredContacts: Computes the list of contacts after applying search queries and filters.
  • Methods:

    • getFirstLetter(name): Retrieves the first letter of a contact's name, considering both English and Chinese characters.
    • scrollToLetter(letter): Scrolls to the section of the contact list that starts with the specified letter.

Explanation:

ContactList.vue enhances user experience by providing powerful search and filtering options. It improves usability with features like alphabetical navigation and favorite contacts filtering. By handling user interactions such as editing and deleting contacts, it directly communicates with the backend API to keep the data consistent.

5.2 Key Backend Code

5.2.1 server.js

// 引入所需模块
const express = require('express');
const cors = require('cors');
const contactRoutes = require('./src/routes/contacts'); // 引入联系人路由
const app = express();
const PORT = 3000; // 服务器端口常量

/**
 * @description 应用中间件配置
 */
app.use(cors()); // 允许跨域请求
app.use(express.json()); // 支持解析 JSON 格式的请求体

/**
 * @description 使用联系人相关的路由
 * @route /api/contacts
 */
app.use('/api/contacts', contactRoutes);

/**
 * @description 启动服务器并监听指定端口
 * @param {number} PORT - 服务器监听的端口
 * @returns {void}
 */
app.listen(PORT, () => {
    console.log(`服务器已启动,访问地址:http://localhost:${PORT}/api/contacts`);
});

Explanation:

  • Module Import: First, the necessary modules are imported using require, including express and cors, as well as the custom contact routes module contactRoutes.

  • Application Configuration:

    • app.use(cors()) allows cross-origin requests, solving the limitations brought by the browser's same-origin policy.
    • app.use(express.json()) middleware is used to parse JSON data in the request body, facilitating the handling of client-sent data.
  • Route Setup: By using app.use('/api/contacts', contactRoutes), all requests starting with /api/contacts are handled by the contactRoutes module, achieving modularization of routes.

  • Server Startup: The server is started using the app.listen(PORT, callback) method and listens on the specified port. After startup, the console outputs the server's access address, which is convenient for debugging and testing.


5.2.2 db.js

// 导入 MySQL 库
const mysql = require('mysql2');

/**
 * @description 创建数据库连接,配置连接参数
 * @param {Object} connectionConfig - 数据库连接配置
 * @param {string} connectionConfig.host - 数据库主机地址
 * @param {string} connectionConfig.user - 数据库用户名
 * @param {string} connectionConfig.password - 数据库密码
 * @param {string} connectionConfig.database - 数据库名称
 * @returns {Object} 数据库连接对象
 */
const db = mysql.createConnection({
    host: 'localhost',  // 数据库主机地址
    user: 'root',       // 数据库用户名
    password: 'Lzp200513', // 数据库密码
    database: 'contacts_db' // 数据库名称
});

/**
 * @description 连接数据库并检查连接状态
 * @returns {void}
 */
db.connect((error) => {
    if (error) {
        console.error('数据库连接失败:', error); // 连接失败输出错误
    } else {
        console.log('已成功连接数据库'); // 连接成功提示
    }
});

// 导出数据库连接对象
module.exports = db;

Explanation:

  • Database Connection Configuration: Uses the mysql2 library to create a connection with the MySQL database. The configuration includes the host, username, password, and database name.

  • Connection Check: Calls the db.connect() method to attempt to connect to the database. If an error occurs, it outputs an error message in the console; if the connection is successful, it outputs a success message.

  • Module Export: Exports the database connection object db so it can be imported and used in other modules (such as controllers).

  • Note: For security reasons, the actual code should properly secure the database password; a placeholder is used here.


5.2.3 contactsController.js

// 引入数据库连接
const db = require('../config/db'); // 引入数据库配置

/**
 * @description 获取所有联系人
 * @route GET /api/contacts
 * @param {Object} request - 请求对象
 * @param {Object} response - 响应对象
 * @returns {void}
 */
exports.getAllContacts = (request, response) => {
    const query = 'SELECT * FROM contacts';
    db.query(query, (error, results) => {
        if (error) {
            console.error('获取联系人失败:', error);
            response.status(500).send('服务器错误');
        } else {
            response.json(results); // 返回所有联系人
        }
    });
};

/**
 * @description 根据 ID 获取联系人
 * @route GET /api/contacts/:id
 * @param {Object} request - 请求对象
 * @param {Object} response - 响应对象
 * @returns {void}
 */
exports.getContactById = (request, response) => {
    const query = 'SELECT * FROM contacts WHERE id = ?';
    db.query(query, [request.params.id], (error, result) => {
        if (error) {
            console.error('获取联系人失败:', error);
            response.status(500).send('服务器错误');
        } else {
            response.json(result[0]); // 返回指定ID的联系人
        }
    });
};

/**
 * @description 根据名字、电话或邮箱查找联系人
 * @route GET /api/contacts/search/:keyword
 * @param {Object} request - 请求对象
 * @param {Object} response - 响应对象
 * @returns {void}
 */
exports.searchContacts = (request, response) => {
    const { keyword } = request.params;
    const query = `
        SELECT * FROM contacts
        WHERE name LIKE ? OR phone LIKE ? OR email LIKE ?
    `;
    const searchValue = `%${keyword}%`; // 模糊搜索
    db.query(query, [searchValue, searchValue, searchValue], (error, results) => {
        if (error) {
            console.error('查找联系人失败:', error);
            response.status(500).send('服务器错误');
        } else {
            response.json(results); // 返回查找结果
        }
    });
};

/**
 * @description 检查联系人是否已存在,用于添加联系人时查重
 * @route POST /api/contacts/check-duplicate
 * @param {Object} request - 请求对象
 * @param {Object} response - 响应对象
 * @returns {void}
 */
exports.checkDuplicateContact = (request, response) => {
    const { name, phone } = request.body;

    // 确保name和phone不为空且为字符串
    if (!name || !phone || typeof name !== 'string' || typeof phone !== 'string') {
        return response.status(400).send('姓名和电话是必填项,且应为字符串');
    }

    const query = 'SELECT * FROM contacts WHERE name = ? OR phone = ?';
    db.query(query, [name, phone], (error, results) => {
        if (error) {
            console.error('检查重复联系人失败:', error);
            return response.status(500).send('服务器错误');
        } else {
            return response.json({ exists: results.length > 0, existingContact: results[0] });
        }
    });
};

/**
 * @description 添加新联系人,包含查重逻辑,避免重复联系人
 * @route POST /api/contacts
 * @param {Object} request - 请求对象
 * @param {Object} response - 响应对象
 * @returns {void}
 */
exports.addContact = (request, response) => {
    const { name, phone, email, group, favorite } = request.body;

    // 确保 name 和 phone 不为空
    if (!name || !phone) {
        return response.status(400).send('姓名和电话是必填项');
    }

    const query = 'INSERT INTO contacts (name, phone, email, `group`, favorite) VALUES (?, ?, ?, ?, ?)';
    db.query(query, [name, phone, email, group, favorite], (error, result) => {
        if (error) {
            console.error('添加联系人失败:', error);
            return response.status(500).send('服务器错误');
        } else {
            response.status(201).json({ id: result.insertId, ...request.body }); // 返回新添加的联系人信息
        }
    });
};

/**
 * @description 更新联系人信息
 * @route PUT /api/contacts/:id
 * @param {Object} request - 请求对象
 * @param {Object} response - 响应对象
 * @returns {void}
 */
exports.updateContact = (request, response) => {
    const { name, phone, email, group, favorite } = request.body;
    const query = 'UPDATE contacts SET name = ?, phone = ?, email = ?, `group` = ?, favorite = ? WHERE id = ?';
    db.query(query, [name, phone, email, group, favorite, request.params.id], (error, result) => {
        if (error) {
            console.error('更新联系人失败:', error);
            response.status(500).send('服务器错误');
        } else {
            response.json({ id: request.params.id, ...request.body }); // 返回更新后的联系人信息
        }
    });
};

/**
 * @description 切换联系人收藏状态
 * @route PATCH /api/contacts/:id/favorite
 * @param {Object} request - 请求对象
 * @param {Object} response - 响应对象
 * @returns {void}
 */
exports.toggleFavorite = (request, response) => {
    const { id } = request.params;
    const { favorite } = request.body; // 从请求体中获取新的 favorite 状态

    const query = 'UPDATE contacts SET favorite = ? WHERE id = ?'; // 更新为传递的收藏状态
    db.query(query, [favorite, id], (error, result) => {
        if (error) {
            console.error('切换收藏状态失败:', error);
            return response.status(500).send('服务器错误');
        } else {
            response.send('收藏状态已更新'); // 成功时返回状态已更新
        }
    });
};

/**
 * @description 删除联系人
 * @route DELETE /api/contacts/:id
 * @param {Object} request - 请求对象
 * @param {Object} response - 响应对象
 * @returns {void}
 */
exports.deleteContact = (request, response) => {
    const query = 'DELETE FROM contacts WHERE id = ?';
    db.query(query, [request.params.id], (error, result) => {
        if (error) {
            console.error('删除联系人失败:', error);
            response.status(500).send('服务器错误');
        } else {
            response.status(204).send(); // 成功后无内容响应
        }
    });
};

Explanation:

  • Get All Contacts: Executes the SELECT * FROM contacts query to retrieve all contact information from the database and returns it in JSON format.

  • Get Contact by ID: Uses a parameterized query to prevent SQL injection, retrieving a specific contact by the id parameter.

  • Add New Contact:

    • Before adding, it checks whether name and phone exist to ensure necessary information is complete.
    • Uses the INSERT INTO statement to insert new contact information into the database.
    • Returns the inserted contact information, including the generated id.
  • Update Contact Information: Uses the UPDATE statement to update the contact information with the specified id, supporting modification of all contact fields.

  • Delete Contact: Uses the DELETE statement to delete the contact with the specified id, and returns status code 204 upon success, indicating that the request has been successfully processed with no content returned.

  • Note: Since group is a reserved keyword in SQL, it needs to be enclosed in backticks in the query.


5.2.4 contacts.js

// 引入模块
const express = require('express');
const router = express.Router();
const contactsController = require('../controller/contactsController'); // 引入联系人控制器

/**
 * @description 定义联系人相关的 API 路由
 * @route GET /api/contacts
 * @returns {Array} 联系人列表
 */
router.get('/', contactsController.getAllContacts); // 获取所有联系人

/**
 * @description 根据 ID 获取联系人
 * @route GET /api/contacts/:id
 * @param {string} id - 联系人ID
 * @returns {Object} 联系人信息
 */
router.get('/:id', contactsController.getContactById); // 根据 ID 获取联系人

/**
 * @description 添加新联系人(包含查重逻辑)
 * @route POST /api/contacts
 * @returns {Object} 新联系人信息
 */
router.post('/', contactsController.addContact); // 添加新联系人(包含查重逻辑)

/**
 * @description 更新联系人信息
 * @route PUT /api/contacts/:id
 * @param {string} id - 联系人ID
 * @returns {Object} 更新后的联系人信息
 */
router.put('/:id', contactsController.updateContact); // 更新联系人

/**
 * @description 删除联系人
 * @route DELETE /api/contacts/:id
 * @param {string} id - 联系人ID
 * @returns {void} 无返回内容
 */
router.delete('/:id', contactsController.deleteContact); // 删除联系人

/**
 * @description 检查重复联系人
 * @route POST /api/contacts/check-duplicate
 * @returns {boolean} 是否存在重复联系人
 */
router.post('/check-duplicate', contactsController.checkDuplicateContact); // 检查重复联系人

/**
 * @description 查找联系人(根据名字、电话或邮箱)
 * @route GET /api/contacts/search/:keyword
 * @param {string} keyword - 搜索关键字
 * @returns {Array} 匹配的联系人列表
 */
router.get('/search/:keyword', contactsController.searchContacts); // 查找联系人

/**
 * @description 切换联系人收藏状态
 * @route PUT /api/contacts/:id/favorite
 * @param {string} id - 联系人ID
 * @returns {string} 收藏状态更新信息
 */
router.put('/:id/favorite', contactsController.toggleFavorite); // 切换联系人收藏状态

// 导出路由
module.exports = router;

Explanation:

  • Modular Routing: Uses Express's routing functionality to define contact-related routes in a separate module, making it convenient to manage and maintain.

  • Route Definitions:

    • GET /: Retrieves all contacts by calling the getAllContacts controller method.
    • GET /:id: Retrieves a contact by ID by calling the getContactById controller method.
    • POST /: Adds a new contact by calling the addContact controller method.
    • PUT /:id: Updates contact information by calling the updateContact controller method.
    • DELETE /:id: Deletes a contact by calling the deleteContact controller method.
  • Module Export: Exports the router module so it can be imported and used in server.js.

6. Personal Learning and Reflections

6.1 Learning Journey

Throughout this project, I gained in-depth knowledge of the frontend-backend separation model. The integration of Vue.js and Express.js helped me understand how to achieve data flow in both directions in a web application, and I also improved my skills in API design and database operations. The project presented various technical challenges, including frontend component-based design, backend API standards, and database operations, all of which were effectively addressed, allowing me to gain valuable project experience.

6.2 Future Improvements

In the future, I plan to enhance the frontend UI design to improve user experience and add more features to the project, such as contact export and batch operations. Additionally, I aim to optimize the backend API to make it more adaptable and extensible, allowing for easier addition of new features in the future.

In summary, this blog outlines the development of my contact management system project. Through this assignment, I successfully built a complete contact management system and deepened my understanding of frontend-backend separation, code standards, API design, and other crucial aspects. Moving forward, I will continue to enhance the functionality and user experience of this project, making it a more convenient tool for contact management.

...全文
84 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

167

社区成员

发帖
与我相关
我的任务
社区描述
2401_MU_SE_FZU
软件工程 高校
社区管理员
  • FZU_SE_TeacherL
  • 助教-吴可仪
  • 助教-孔志豪
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧