Skip to content

综合实战项目(完整功能)

21.1 学生信息管理系统

项目介绍

学生信息管理系统是一个用于管理学生信息的应用,可以添加、编辑、删除和查询学生信息。

实现思路

  1. 设计系统的 UI 界面
  2. 实现学生信息的添加功能
  3. 实现学生信息的编辑功能
  4. 实现学生信息的删除功能
  5. 实现学生信息的查询功能
  6. 使用本地存储保存学生数据

完整代码

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>学生信息管理系统</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    body {
      font-family: Arial, sans-serif;
      background-color: #f4f4f4;
      padding: 20px;
    }
    .container {
      max-width: 1000px;
      margin: 0 auto;
      background-color: white;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
      overflow: hidden;
    }
    header {
      background-color: #3498db;
      color: white;
      padding: 20px;
      text-align: center;
    }
    h1 {
      margin-bottom: 10px;
    }
    .controls {
      padding: 20px;
      background-color: #f9f9f9;
      border-bottom: 1px solid #ddd;
    }
    .search-add {
      display: flex;
      gap: 10px;
      margin-bottom: 20px;
    }
    input[type="text"] {
      flex: 1;
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 4px;
      font-size: 16px;
    }
    button {
      padding: 10px 20px;
      border: none;
      border-radius: 4px;
      font-size: 16px;
      cursor: pointer;
      transition: background-color 0.2s;
    }
    .add-btn {
      background-color: #4CAF50;
      color: white;
    }
    .add-btn:hover {
      background-color: #45a049;
    }
    .table-container {
      padding: 20px;
      overflow-x: auto;
    }
    table {
      width: 100%;
      border-collapse: collapse;
    }
    th, td {
      padding: 12px;
      text-align: left;
      border-bottom: 1px solid #ddd;
    }
    th {
      background-color: #f2f2f2;
      font-weight: bold;
    }
    tr:hover {
      background-color: #f5f5f5;
    }
    .action-buttons {
      display: flex;
      gap: 5px;
    }
    .edit-btn {
      background-color: #f39c12;
      color: white;
      padding: 5px 10px;
      font-size: 14px;
    }
    .edit-btn:hover {
      background-color: #e67e22;
    }
    .delete-btn {
      background-color: #e74c3c;
      color: white;
      padding: 5px 10px;
      font-size: 14px;
    }
    .delete-btn:hover {
      background-color: #c0392b;
    }
    .modal {
      display: none;
      position: fixed;
      z-index: 1;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      overflow: auto;
      background-color: rgba(0, 0, 0, 0.4);
    }
    .modal-content {
      background-color: white;
      margin: 15% auto;
      padding: 20px;
      border: 1px solid #888;
      width: 80%;
      max-width: 500px;
      border-radius: 8px;
      box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
    }
    .modal-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 20px;
    }
    .close {
      color: #aaa;
      float: right;
      font-size: 28px;
      font-weight: bold;
      cursor: pointer;
    }
    .close:hover {
      color: black;
    }
    .form-group {
      margin-bottom: 15px;
    }
    label {
      display: block;
      margin-bottom: 5px;
      font-weight: bold;
    }
    .form-actions {
      display: flex;
      gap: 10px;
      justify-content: flex-end;
      margin-top: 20px;
    }
    .save-btn {
      background-color: #4CAF50;
      color: white;
    }
    .save-btn:hover {
      background-color: #45a049;
    }
    .cancel-btn {
      background-color: #95a5a6;
      color: white;
    }
    .cancel-btn:hover {
      background-color: #7f8c8d;
    }
    .empty-state {
      text-align: center;
      padding: 40px 20px;
      color: #999;
      font-size: 18px;
    }
  </style>
</head>
<body>
  <div class="container">
    <header>
      <h1>学生信息管理系统</h1>
      <p>管理学生信息,包括添加、编辑、删除和查询</p>
    </header>
    
    <div class="controls">
      <div class="search-add">
        <input type="text" id="searchInput" placeholder="搜索学生姓名或学号">
        <button class="add-btn" id="addBtn">添加学生</button>
      </div>
    </div>
    
    <div class="table-container">
      <table id="studentTable">
        <thead>
          <tr>
            <th>学号</th>
            <th>姓名</th>
            <th>性别</th>
            <th>年龄</th>
            <th>班级</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody id="studentTableBody">
          <!-- 学生信息将通过 JavaScript 动态添加 -->
        </tbody>
      </table>
    </div>
  </div>
  
  <!-- 添加/编辑学生模态框 -->
  <div id="studentModal" class="modal">
    <div class="modal-content">
      <div class="modal-header">
        <h2 id="modalTitle">添加学生</h2>
        <span class="close">&times;</span>
      </div>
      <form id="studentForm">
        <input type="hidden" id="studentId">
        <div class="form-group">
          <label for="studentNo">学号</label>
          <input type="text" id="studentNo" required>
        </div>
        <div class="form-group">
          <label for="studentName">姓名</label>
          <input type="text" id="studentName" required>
        </div>
        <div class="form-group">
          <label for="studentGender">性别</label>
          <select id="studentGender" required>
            <option value="男">男</option>
            <option value="女">女</option>
          </select>
        </div>
        <div class="form-group">
          <label for="studentAge">年龄</label>
          <input type="number" id="studentAge" min="1" required>
        </div>
        <div class="form-group">
          <label for="studentClass">班级</label>
          <input type="text" id="studentClass" required>
        </div>
        <div class="form-actions">
          <button type="button" class="cancel-btn" id="cancelBtn">取消</button>
          <button type="submit" class="save-btn">保存</button>
        </div>
      </form>
    </div>
  </div>

  <script>
    // 获取 DOM 元素
    const addBtn = document.getElementById("addBtn");
    const searchInput = document.getElementById("searchInput");
    const studentTableBody = document.getElementById("studentTableBody");
    const modal = document.getElementById("studentModal");
    const modalTitle = document.getElementById("modalTitle");
    const studentForm = document.getElementById("studentForm");
    const studentId = document.getElementById("studentId");
    const studentNo = document.getElementById("studentNo");
    const studentName = document.getElementById("studentName");
    const studentGender = document.getElementById("studentGender");
    const studentAge = document.getElementById("studentAge");
    const studentClass = document.getElementById("studentClass");
    const cancelBtn = document.getElementById("cancelBtn");
    const closeModal = document.querySelector(".close");

    // 学生数据
    let students = JSON.parse(localStorage.getItem("students")) || [];
    let editingId = null;

    // 渲染学生列表
    function renderStudents(filteredStudents = students) {
      studentTableBody.innerHTML = "";
      
      if (filteredStudents.length === 0) {
        const emptyRow = document.createElement("tr");
        emptyRow.innerHTML = `<td colspan="6" class="empty-state">暂无学生信息</td>`;
        studentTableBody.appendChild(emptyRow);
        return;
      }
      
      filteredStudents.forEach(student => {
        const row = document.createElement("tr");
        row.innerHTML = `
          <td>${student.no}</td>
          <td>${student.name}</td>
          <td>${student.gender}</td>
          <td>${student.age}</td>
          <td>${student.class}</td>
          <td class="action-buttons">
            <button class="edit-btn" data-id="${student.id}">编辑</button>
            <button class="delete-btn" data-id="${student.id}">删除</button>
          </td>
        `;
        studentTableBody.appendChild(row);
      });
      
      // 添加编辑和删除事件
      document.querySelectorAll(".edit-btn").forEach(btn => {
        btn.addEventListener("click", function() {
          const id = this.dataset.id;
          editStudent(id);
        });
      });
      
      document.querySelectorAll(".delete-btn").forEach(btn => {
        btn.addEventListener("click", function() {
          const id = this.dataset.id;
          deleteStudent(id);
        });
      });
    }

    // 保存学生数据到本地存储
    function saveStudents() {
      localStorage.setItem("students", JSON.stringify(students));
    }

    // 打开添加学生模态框
    function openAddModal() {
      modalTitle.textContent = "添加学生";
      studentForm.reset();
      studentId.value = "";
      editingId = null;
      modal.style.display = "block";
    }

    // 打开编辑学生模态框
    function editStudent(id) {
      const student = students.find(s => s.id === id);
      if (student) {
        modalTitle.textContent = "编辑学生";
        studentId.value = student.id;
        studentNo.value = student.no;
        studentName.value = student.name;
        studentGender.value = student.gender;
        studentAge.value = student.age;
        studentClass.value = student.class;
        editingId = id;
        modal.style.display = "block";
      }
    }

    // 添加学生
    function addStudent(student) {
      student.id = Date.now().toString();
      students.push(student);
      saveStudents();
      renderStudents();
    }

    // 更新学生
    function updateStudent(id, updatedStudent) {
      const index = students.findIndex(s => s.id === id);
      if (index !== -1) {
        students[index] = { ...students[index], ...updatedStudent };
        saveStudents();
        renderStudents();
      }
    }

    // 删除学生
    function deleteStudent(id) {
      if (confirm("确定要删除这个学生吗?")) {
        students = students.filter(s => s.id !== id);
        saveStudents();
        renderStudents();
      }
    }

    // 搜索学生
    function searchStudents() {
      const searchTerm = searchInput.value.toLowerCase();
      const filteredStudents = students.filter(student => 
        student.name.toLowerCase().includes(searchTerm) || 
        student.no.toLowerCase().includes(searchTerm)
      );
      renderStudents(filteredStudents);
    }

    // 关闭模态框
    function closeModalFunc() {
      modal.style.display = "none";
    }

    // 事件监听
    addBtn.addEventListener("click", openAddModal);
    closeModal.addEventListener("click", closeModalFunc);
    cancelBtn.addEventListener("click", closeModalFunc);
    searchInput.addEventListener("input", searchStudents);

    // 点击模态框外部关闭
    window.addEventListener("click", function(event) {
      if (event.target === modal) {
        closeModalFunc();
      }
    });

    // 表单提交
    studentForm.addEventListener("submit", function(event) {
      event.preventDefault();
      
      const studentData = {
        no: studentNo.value,
        name: studentName.value,
        gender: studentGender.value,
        age: studentAge.value,
        class: studentClass.value
      };
      
      if (editingId) {
        updateStudent(editingId, studentData);
      } else {
        addStudent(studentData);
      }
      
      closeModalFunc();
    });

    // 初始渲染
    renderStudents();
  </script>
</body>
</html>

21.2 响应式购物车

项目介绍

响应式购物车可以在不同设备上正常显示,用户可以添加商品到购物车、修改商品数量和删除商品。

实现思路

  1. 设计响应式的购物车界面
  2. 实现商品的添加功能
  3. 实现商品数量的修改功能
  4. 实现商品的删除功能
  5. 实现购物车总价的计算
  6. 使用本地存储保存购物车数据

完整代码

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>响应式购物车</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    body {
      font-family: Arial, sans-serif;
      background-color: #f4f4f4;
      padding: 20px;
    }
    .container {
      max-width: 1200px;
      margin: 0 auto;
    }
    h1 {
      text-align: center;
      margin-bottom: 30px;
      color: #333;
    }
    .products {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
      gap: 20px;
      margin-bottom: 40px;
    }
    .product {
      background-color: white;
      border-radius: 8px;
      padding: 20px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
      transition: transform 0.2s;
    }
    .product:hover {
      transform: translateY(-5px);
    }
    .product img {
      width: 100%;
      height: 200px;
      object-fit: cover;
      border-radius: 4px;
      margin-bottom: 15px;
    }
    .product h3 {
      margin-bottom: 10px;
      color: #333;
    }
    .product .price {
      font-size: 20px;
      font-weight: bold;
      color: #f44336;
      margin-bottom: 15px;
    }
    .add-to-cart {
      background-color: #4CAF50;
      color: white;
      border: none;
      padding: 10px 15px;
      border-radius: 4px;
      font-size: 16px;
      cursor: pointer;
      width: 100%;
      transition: background-color 0.2s;
    }
    .add-to-cart:hover {
      background-color: #45a049;
    }
    .cart {
      background-color: white;
      border-radius: 8px;
      padding: 20px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    }
    .cart h2 {
      margin-bottom: 20px;
      color: #333;
    }
    .cart-items {
      margin-bottom: 20px;
    }
    .cart-item {
      display: flex;
      align-items: center;
      padding: 15px 0;
      border-bottom: 1px solid #eee;
    }
    .cart-item:last-child {
      border-bottom: none;
    }
    .cart-item img {
      width: 80px;
      height: 80px;
      object-fit: cover;
      border-radius: 4px;
      margin-right: 15px;
    }
    .cart-item-info {
      flex: 1;
    }
    .cart-item-info h4 {
      margin-bottom: 5px;
      color: #333;
    }
    .cart-item-price {
      font-weight: bold;
      color: #f44336;
      margin-bottom: 10px;
    }
    .cart-item-quantity {
      display: flex;
      align-items: center;
      gap: 10px;
    }
    .quantity-btn {
      width: 30px;
      height: 30px;
      border: 1px solid #ddd;
      background-color: #f9f9f9;
      border-radius: 4px;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .quantity-input {
      width: 50px;
      text-align: center;
      border: 1px solid #ddd;
      border-radius: 4px;
      padding: 5px;
    }
    .remove-btn {
      background-color: #f44336;
      color: white;
      border: none;
      padding: 5px 10px;
      border-radius: 4px;
      cursor: pointer;
      font-size: 14px;
    }
    .remove-btn:hover {
      background-color: #d32f2f;
    }
    .cart-total {
      text-align: right;
      padding-top: 20px;
      border-top: 1px solid #eee;
    }
    .total-label {
      font-size: 18px;
      font-weight: bold;
      margin-right: 10px;
    }
    .total-price {
      font-size: 24px;
      font-weight: bold;
      color: #f44336;
    }
    .checkout-btn {
      background-color: #3498db;
      color: white;
      border: none;
      padding: 15px 30px;
      border-radius: 4px;
      font-size: 18px;
      cursor: pointer;
      margin-top: 20px;
      width: 100%;
      transition: background-color 0.2s;
    }
    .checkout-btn:hover {
      background-color: #2980b9;
    }
    .empty-cart {
      text-align: center;
      padding: 40px 20px;
      color: #999;
      font-size: 18px;
    }
    @media (max-width: 768px) {
      .cart-item {
        flex-direction: column;
        align-items: flex-start;
        gap: 10px;
      }
      .cart-item img {
        width: 100px;
        height: 100px;
      }
      .cart-item-quantity {
        align-self: flex-end;
      }
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>响应式购物车</h1>
    
    <div class="products" id="products">
      <div class="product">
        <img src="https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=smartphone%20product%20photo&image_size=square" alt="智能手机">
        <h3>智能手机</h3>
        <div class="price">¥3999</div>
        <button class="add-to-cart" data-id="1" data-name="智能手机" data-price="3999" data-image="https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=smartphone%20product%20photo&image_size=square">添加到购物车</button>
      </div>
      <div class="product">
        <img src="https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=laptop%20product%20photo&image_size=square" alt="笔记本电脑">
        <h3>笔记本电脑</h3>
        <div class="price">¥5999</div>
        <button class="add-to-cart" data-id="2" data-name="笔记本电脑" data-price="5999" data-image="https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=laptop%20product%20photo&image_size=square">添加到购物车</button>
      </div>
      <div class="product">
        <img src="https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=headphones%20product%20photo&image_size=square" alt="耳机">
        <h3>耳机</h3>
        <div class="price">¥899</div>
        <button class="add-to-cart" data-id="3" data-name="耳机" data-price="899" data-image="https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=headphones%20product%20photo&image_size=square">添加到购物车</button>
      </div>
      <div class="product">
        <img src="https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=smart%20watch%20product%20photo&image_size=square" alt="智能手表">
        <h3>智能手表</h3>
        <div class="price">¥1299</div>
        <button class="add-to-cart" data-id="4" data-name="智能手表" data-price="1299" data-image="https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=smart%20watch%20product%20photo&image_size=square">添加到购物车</button>
      </div>
    </div>
    
    <div class="cart">
      <h2>购物车</h2>
      <div class="cart-items" id="cartItems">
        <!-- 购物车商品将通过 JavaScript 动态添加 -->
      </div>
      <div class="cart-total">
        <span class="total-label">总计:</span>
        <span class="total-price" id="totalPrice">¥0</span>
      </div>
      <button class="checkout-btn" id="checkoutBtn">结算</button>
    </div>
  </div>

  <script>
    // 获取 DOM 元素
    const addToCartButtons = document.querySelectorAll(".add-to-cart");
    const cartItems = document.getElementById("cartItems");
    const totalPrice = document.getElementById("totalPrice");
    const checkoutBtn = document.getElementById("checkoutBtn");

    // 购物车数据
    let cart = JSON.parse(localStorage.getItem("cart")) || [];

    // 渲染购物车
    function renderCart() {
      cartItems.innerHTML = "";
      
      if (cart.length === 0) {
        cartItems.innerHTML = '<div class="empty-cart">购物车为空</div>';
        totalPrice.textContent = "¥0";
        return;
      }
      
      let total = 0;
      
      cart.forEach(item => {
        const itemTotal = item.price * item.quantity;
        total += itemTotal;
        
        const cartItem = document.createElement("div");
        cartItem.className = "cart-item";
        cartItem.innerHTML = `
          <img src="${item.image}" alt="${item.name}">
          <div class="cart-item-info">
            <h4>${item.name}</h4>
            <div class="cart-item-price">¥${item.price}</div>
            <div class="cart-item-quantity">
              <button class="quantity-btn decrease" data-id="${item.id}">-</button>
              <input type="number" class="quantity-input" value="${item.quantity}" min="1" data-id="${item.id}">
              <button class="quantity-btn increase" data-id="${item.id}">+</button>
              <button class="remove-btn" data-id="${item.id}">删除</button>
            </div>
          </div>
        `;
        
        cartItems.appendChild(cartItem);
      });
      
      totalPrice.textContent = `¥${total}`;
      
      // 添加事件监听
      addCartItemEvents();
    }

    // 添加购物车项目事件
    function addCartItemEvents() {
      // 减少数量
      document.querySelectorAll(".decrease").forEach(btn => {
        btn.addEventListener("click", function() {
          const id = this.dataset.id;
          updateQuantity(id, -1);
        });
      });
      
      // 增加数量
      document.querySelectorAll(".increase").forEach(btn => {
        btn.addEventListener("click", function() {
          const id = this.dataset.id;
          updateQuantity(id, 1);
        });
      });
      
      // 输入数量
      document.querySelectorAll(".quantity-input").forEach(input => {
        input.addEventListener("change", function() {
          const id = this.dataset.id;
          const quantity = parseInt(this.value) || 1;
          updateQuantity(id, quantity - cart.find(item => item.id === id).quantity);
        });
      });
      
      // 删除商品
      document.querySelectorAll(".remove-btn").forEach(btn => {
        btn.addEventListener("click", function() {
          const id = this.dataset.id;
          removeFromCart(id);
        });
      });
    }

    // 添加商品到购物车
    function addToCart(product) {
      const existingItem = cart.find(item => item.id === product.id);
      
      if (existingItem) {
        existingItem.quantity++;
      } else {
        cart.push({
          id: product.id,
          name: product.name,
          price: parseFloat(product.price),
          quantity: 1,
          image: product.image
        });
      }
      
      saveCart();
      renderCart();
    }

    // 更新商品数量
    function updateQuantity(id, change) {
      const item = cart.find(item => item.id === id);
      if (item) {
        item.quantity = Math.max(1, item.quantity + change);
        saveCart();
        renderCart();
      }
    }

    // 从购物车中删除商品
    function removeFromCart(id) {
      cart = cart.filter(item => item.id !== id);
      saveCart();
      renderCart();
    }

    // 保存购物车到本地存储
    function saveCart() {
      localStorage.setItem("cart", JSON.stringify(cart));
    }

    // 结算
    function checkout() {
      if (cart.length === 0) {
        alert("购物车为空,无法结算");
        return;
      }
      
      alert("结算成功!");
      cart = [];
      saveCart();
      renderCart();
    }

    // 事件监听
    addToCartButtons.forEach(btn => {
      btn.addEventListener("click", function() {
        const product = {
          id: this.dataset.id,
          name: this.dataset.name,
          price: this.dataset.price,
          image: this.dataset.image
        };
        addToCart(product);
      });
    });

    checkoutBtn.addEventListener("click", checkout);

    // 初始渲染
    renderCart();
  </script>
</body>
</html>

21.3 天气查询小工具

项目介绍

天气查询小工具可以根据用户输入的城市名称查询天气信息,并显示当前天气和未来几天的天气预报。

实现思路

  1. 设计天气查询的 UI 界面
  2. 实现城市名称的输入和提交
  3. 使用天气 API 获取天气数据
  4. 显示当前天气信息
  5. 显示未来几天的天气预报
  6. 处理错误情况

完整代码

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>天气查询小工具</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    body {
      font-family: Arial, sans-serif;
      background-color: #f4f4f4;
      padding: 20px;
      min-height: 100vh;
      background-image: linear-gradient(to bottom, #3498db, #2980b9);
      color: white;
    }
    .container {
      max-width: 800px;
      margin: 0 auto;
    }
    h1 {
      text-align: center;
      margin-bottom: 30px;
    }
    .search-container {
      display: flex;
      gap: 10px;
      margin-bottom: 30px;
    }
    input[type="text"] {
      flex: 1;
      padding: 15px;
      border: none;
      border-radius: 4px;
      font-size: 16px;
    }
    button {
      padding: 15px 25px;
      border: none;
      border-radius: 4px;
      font-size: 16px;
      cursor: pointer;
      transition: background-color 0.2s;
    }
    .search-btn {
      background-color: #4CAF50;
      color: white;
    }
    .search-btn:hover {
      background-color: #45a049;
    }
    .weather-card {
      background-color: rgba(255, 255, 255, 0.1);
      backdrop-filter: blur(10px);
      border-radius: 10px;
      padding: 20px;
      margin-bottom: 20px;
    }
    .current-weather {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 30px;
    }
    .weather-info {
      flex: 1;
    }
    .city-name {
      font-size: 32px;
      font-weight: bold;
      margin-bottom: 10px;
    }
    .weather-description {
      font-size: 18px;
      margin-bottom: 10px;
    }
    .temperature {
      font-size: 48px;
      font-weight: bold;
      margin-bottom: 10px;
    }
    .weather-details {
      display: flex;
      gap: 20px;
      margin-bottom: 20px;
    }
    .detail {
      display: flex;
      flex-direction: column;
    }
    .detail-label {
      font-size: 14px;
      opacity: 0.8;
    }
    .detail-value {
      font-size: 18px;
      font-weight: bold;
    }
    .weather-icon {
      font-size: 80px;
    }
    .forecast {
      margin-top: 30px;
    }
    .forecast h2 {
      margin-bottom: 20px;
      font-size: 24px;
    }
    .forecast-cards {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
      gap: 15px;
    }
    .forecast-card {
      background-color: rgba(255, 255, 255, 0.1);
      border-radius: 8px;
      padding: 15px;
      text-align: center;
    }
    .forecast-date {
      font-size: 14px;
      margin-bottom: 10px;
    }
    .forecast-icon {
      font-size: 32px;
      margin-bottom: 10px;
    }
    .forecast-temp {
      font-size: 16px;
      font-weight: bold;
    }
    .error-message {
      background-color: rgba(255, 70, 70, 0.2);
      border: 1px solid rgba(255, 70, 70, 0.5);
      border-radius: 4px;
      padding: 15px;
      margin-bottom: 20px;
      text-align: center;
    }
    .loading {
      text-align: center;
      padding: 40px;
      font-size: 18px;
    }
    @media (max-width: 768px) {
      .current-weather {
        flex-direction: column;
        align-items: flex-start;
        gap: 20px;
      }
      .weather-icon {
        font-size: 60px;
      }
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>天气查询小工具</h1>
    
    <div class="search-container">
      <input type="text" id="cityInput" placeholder="请输入城市名称">
      <button class="search-btn" id="searchBtn">查询</button>
    </div>
    
    <div id="weatherResult">
      <!-- 天气结果将通过 JavaScript 动态添加 -->
    </div>
  </div>

  <script>
    // 获取 DOM 元素
    const cityInput = document.getElementById("cityInput");
    const searchBtn = document.getElementById("searchBtn");
    const weatherResult = document.getElementById("weatherResult");

    // 天气 API 密钥(注意:在实际项目中,应该将 API 密钥存储在服务器端)
    const apiKey = "YOUR_API_KEY"; // 请替换为真实的 API 密钥

    // 搜索天气
    async function searchWeather() {
      const city = cityInput.value.trim();
      if (!city) {
        showError("请输入城市名称");
        return;
      }

      try {
        showLoading();
        
        // 使用 OpenWeatherMap API 获取天气数据
        // 注意:这里使用的是模拟数据,实际项目中需要替换为真实的 API 调用
        // const response = await fetch(`https://api.openweathermap.org/data/2.5/forecast?q=${city}&appid=${apiKey}&units=metric&lang=zh_cn`);
        // const data = await response.json();
        
        // 模拟天气数据
        const data = getMockWeatherData(city);
        
        if (data.cod === "404") {
          showError("未找到该城市的天气信息");
          return;
        }
        
        displayWeather(data);
      } catch (error) {
        console.error("获取天气数据失败:", error);
        showError("获取天气数据失败,请稍后重试");
      }
    }

    // 显示加载状态
    function showLoading() {
      weatherResult.innerHTML = '<div class="loading">加载中...</div>';
    }

    // 显示错误信息
    function showError(message) {
      weatherResult.innerHTML = `<div class="error-message">${message}</div>`;
    }

    // 显示天气信息
    function displayWeather(data) {
      const currentWeather = data.list[0];
      const forecast = data.list.filter((_, index) => index % 8 === 0);
      
      let html = `
        <div class="weather-card">
          <div class="current-weather">
            <div class="weather-info">
              <div class="city-name">${data.city.name}, ${data.city.country}</div>
              <div class="weather-description">${currentWeather.weather[0].description}</div>
              <div class="temperature">${Math.round(currentWeather.main.temp)}°C</div>
              <div class="weather-details">
                <div class="detail">
                  <span class="detail-label">体感温度</span>
                  <span class="detail-value">${Math.round(currentWeather.main.feels_like)}°C</span>
                </div>
                <div class="detail">
                  <span class="detail-label">湿度</span>
                  <span class="detail-value">${currentWeather.main.humidity}%</span>
                </div>
                <div class="detail">
                  <span class="detail-label">风速</span>
                  <span class="detail-value">${currentWeather.wind.speed} m/s</span>
                </div>
              </div>
            </div>
            <div class="weather-icon">🌤️</div>
          </div>
          
          <div class="forecast">
            <h2>未来天气预报</h2>
            <div class="forecast-cards">
      `;
      
      forecast.forEach(item => {
        const date = new Date(item.dt * 1000);
        const day = date.toLocaleDateString("zh-CN", { weekday: "short" });
        
        html += `
          <div class="forecast-card">
            <div class="forecast-date">${day}</div>
            <div class="forecast-icon">🌤️</div>
            <div class="forecast-temp">${Math.round(item.main.temp)}°C</div>
          </div>
        `;
      });
      
      html += `
            </div>
          </div>
        </div>
      `;
      
      weatherResult.innerHTML = html;
    }

    // 模拟天气数据
    function getMockWeatherData(city) {
      return {
        cod: "200",
        city: {
          name: city,
          country: "CN"
        },
        list: [
          {
            dt: Math.floor(Date.now() / 1000),
            main: {
              temp: 25,
              feels_like: 26,
              humidity: 60
            },
            weather: [{
              description: "晴"
            }],
            wind: {
              speed: 5
            }
          },
          {
            dt: Math.floor(Date.now() / 1000) + 86400,
            main: {
              temp: 24,
              feels_like: 25,
              humidity: 55
            },
            weather: [{
              description: "多云"
            }],
            wind: {
              speed: 4
            }
          },
          {
            dt: Math.floor(Date.now() / 1000) + 86400 * 2,
            main: {
              temp: 23,
              feels_like: 24,
              humidity: 65
            },
            weather: [{
              description: "小雨"
            }],
            wind: {
              speed: 6
            }
          },
          {
            dt: Math.floor(Date.now() / 1000) + 86400 * 3,
            main: {
              temp: 22,
              feels_like: 23,
              humidity: 70
            },
            weather: [{
              description: "阴"
            }],
            wind: {
              speed: 3
            }
          },
          {
            dt: Math.floor(Date.now() / 1000) + 86400 * 4,
            main: {
              temp: 24,
              feels_like: 25,
              humidity: 50
            },
            weather: [{
              description: "晴"
            }],
            wind: {
              speed: 4
            }
          }
        ]
      };
    }

    // 事件监听
    searchBtn.addEventListener("click", searchWeather);
    cityInput.addEventListener("keypress", function(event) {
      if (event.key === "Enter") {
        searchWeather();
      }
    });
  </script>
</body>
</html>

21.4 个人简历生成器

项目介绍

个人简历生成器可以帮助用户创建和下载个人简历,用户可以输入个人信息、教育背景、工作经历等内容。

实现思路

  1. 设计简历生成器的 UI 界面
  2. 实现个人信息的输入功能
  3. 实现教育背景的添加和删除功能
  4. 实现工作经历的添加和删除功能
  5. 实现简历的预览功能
  6. 实现简历的下载功能

完整代码

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>个人简历生成器</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    body {
      font-family: Arial, sans-serif;
      background-color: #f4f4f4;
      padding: 20px;
    }
    .container {
      max-width: 1200px;
      margin: 0 auto;
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 30px;
    }
    h1 {
      grid-column: 1 / -1;
      text-align: center;
      margin-bottom: 30px;
      color: #333;
    }
    .form-section {
      background-color: white;
      border-radius: 8px;
      padding: 20px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    }
    h2 {
      margin-bottom: 20px;
      color: #333;
      border-bottom: 2px solid #3498db;
      padding-bottom: 10px;
    }
    .form-group {
      margin-bottom: 15px;
    }
    label {
      display: block;
      margin-bottom: 5px;
      font-weight: bold;
      color: #555;
    }
    input[type="text"],
    input[type="email"],
    input[type="tel"],
    textarea,
    select {
      width: 100%;
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 4px;
      font-size: 16px;
    }
    textarea {
      resize: vertical;
      min-height: 100px;
    }
    .button-group {
      display: flex;
      gap: 10px;
      margin-bottom: 20px;
    }
    button {
      padding: 10px 20px;
      border: none;
      border-radius: 4px;
      font-size: 16px;
      cursor: pointer;
      transition: background-color 0.2s;
    }
    .add-btn {
      background-color: #4CAF50;
      color: white;
    }
    .add-btn:hover {
      background-color: #45a049;
    }
    .remove-btn {
      background-color: #f44336;
      color: white;
      padding: 5px 10px;
      font-size: 14px;
    }
    .remove-btn:hover {
      background-color: #d32f2f;
    }
    .section-item {
      background-color: #f9f9f9;
      padding: 15px;
      border-radius: 4px;
      margin-bottom: 10px;
      border-left: 4px solid #3498db;
    }
    .section-item h3 {
      margin-bottom: 10px;
      color: #333;
    }
    .preview-section {
      background-color: white;
      border-radius: 8px;
      padding: 40px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
      min-height: 800px;
      position: relative;
    }
    .resume-header {
      text-align: center;
      margin-bottom: 40px;
      padding-bottom: 20px;
      border-bottom: 2px solid #333;
    }
    .resume-name {
      font-size: 32px;
      font-weight: bold;
      margin-bottom: 10px;
    }
    .resume-contact {
      display: flex;
      justify-content: center;
      gap: 20px;
      flex-wrap: wrap;
      font-size: 14px;
      color: #666;
    }
    .resume-section {
      margin-bottom: 30px;
    }
    .resume-section h2 {
      font-size: 20px;
      margin-bottom: 15px;
      border-bottom: 1px solid #ddd;
      padding-bottom: 5px;
    }
    .resume-item {
      margin-bottom: 15px;
    }
    .resume-item-header {
      display: flex;
      justify-content: space-between;
      margin-bottom: 5px;
    }
    .resume-item-title {
      font-weight: bold;
    }
    .resume-item-date {
      color: #666;
      font-size: 14px;
    }
    .resume-item-company {
      font-size: 14px;
      color: #666;
      margin-bottom: 5px;
    }
    .resume-item-description {
      font-size: 14px;
      line-height: 1.5;
    }
    .download-btn {
      background-color: #3498db;
      color: white;
      padding: 15px 30px;
      font-size: 18px;
      position: absolute;
      bottom: 20px;
      right: 20px;
    }
    .download-btn:hover {
      background-color: #2980b9;
    }
    @media (max-width: 768px) {
      .container {
        grid-template-columns: 1fr;
      }
      .preview-section {
        order: -1;
      }
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>个人简历生成器</h1>
    
    <div class="form-section">
      <h2>个人信息</h2>
      <div class="form-group">
        <label for="name">姓名</label>
        <input type="text" id="name" placeholder="请输入姓名">
      </div>
      <div class="form-group">
        <label for="email">邮箱</label>
        <input type="email" id="email" placeholder="请输入邮箱">
      </div>
      <div class="form-group">
        <label for="phone">电话</label>
        <input type="tel" id="phone" placeholder="请输入电话">
      </div>
      <div class="form-group">
        <label for="address">地址</label>
        <input type="text" id="address" placeholder="请输入地址">
      </div>
      <div class="form-group">
        <label for="summary">个人简介</label>
        <textarea id="summary" placeholder="请输入个人简介"></textarea>
      </div>
      
      <h2>教育背景</h2>
      <div class="button-group">
        <button class="add-btn" id="addEducation">添加教育背景</button>
      </div>
      <div id="educationList">
        <!-- 教育背景将通过 JavaScript 动态添加 -->
      </div>
      
      <h2>工作经历</h2>
      <div class="button-group">
        <button class="add-btn" id="addExperience">添加工作经历</button>
      </div>
      <div id="experienceList">
        <!-- 工作经历将通过 JavaScript 动态添加 -->
      </div>
      
      <h2>技能</h2>
      <div class="form-group">
        <label for="skills">技能(用逗号分隔)</label>
        <input type="text" id="skills" placeholder="例如:JavaScript, HTML, CSS">
      </div>
    </div>
    
    <div class="preview-section" id="resumePreview">
      <div class="resume-header">
        <div class="resume-name">姓名</div>
        <div class="resume-contact">
          <span id="previewEmail">邮箱</span>
          <span id="previewPhone">电话</span>
          <span id="previewAddress">地址</span>
        </div>
      </div>
      
      <div class="resume-section">
        <h2>个人简介</h2>
        <div id="previewSummary">个人简介</div>
      </div>
      
      <div class="resume-section">
        <h2>教育背景</h2>
        <div id="previewEducation">教育背景</div>
      </div>
      
      <div class="resume-section">
        <h2>工作经历</h2>
        <div id="previewExperience">工作经历</div>
      </div>
      
      <div class="resume-section">
        <h2>技能</h2>
        <div id="previewSkills">技能</div>
      </div>
      
      <button class="download-btn" id="downloadBtn">下载简历</button>
    </div>
  </div>

  <script>
    // 获取 DOM 元素
    const nameInput = document.getElementById("name");
    const emailInput = document.getElementById("email");
    const phoneInput = document.getElementById("phone");
    const addressInput = document.getElementById("address");
    const summaryInput = document.getElementById("summary");
    const skillsInput = document.getElementById("skills");
    const addEducationBtn = document.getElementById("addEducation");
    const addExperienceBtn = document.getElementById("addExperience");
    const educationList = document.getElementById("educationList");
    const experienceList = document.getElementById("experienceList");
    const resumePreview = document.getElementById("resumePreview");
    const downloadBtn = document.getElementById("downloadBtn");

    // 数据
    let educationItems = [];
    let experienceItems = [];

    // 添加教育背景
    function addEducation() {
      const id = Date.now().toString();
      const education = {
        id,
        school: "",
        degree: "",
        field: "",
        startDate: "",
        endDate: "",
        description: ""
      };
      educationItems.push(education);
      renderEducationList();
    }

    // 添加工作经历
    function addExperience() {
      const id = Date.now().toString();
      const experience = {
        id,
        company: "",
        position: "",
        startDate: "",
        endDate: "",
        description: ""
      };
      experienceItems.push(experience);
      renderExperienceList();
    }

    // 渲染教育背景列表
    function renderEducationList() {
      educationList.innerHTML = "";
      educationItems.forEach((item, index) => {
        const educationItem = document.createElement("div");
        educationItem.className = "section-item";
        educationItem.innerHTML = `
          <h3>教育背景 ${index + 1}</h3>
          <div class="form-group">
            <label for="education-school-${item.id}">学校</label>
            <input type="text" id="education-school-${item.id}" value="${item.school}" placeholder="请输入学校名称">
          </div>
          <div class="form-group">
            <label for="education-degree-${item.id}">学位</label>
            <input type="text" id="education-degree-${item.id}" value="${item.degree}" placeholder="请输入学位">
          </div>
          <div class="form-group">
            <label for="education-field-${item.id}">专业</label>
            <input type="text" id="education-field-${item.id}" value="${item.field}" placeholder="请输入专业">
          </div>
          <div class="button-group">
            <div class="form-group" style="flex: 1;">
              <label for="education-start-${item.id}">开始日期</label>
              <input type="text" id="education-start-${item.id}" value="${item.startDate}" placeholder="例如:2020年9月">
            </div>
            <div class="form-group" style="flex: 1;">
              <label for="education-end-${item.id}">结束日期</label>
              <input type="text" id="education-end-${item.id}" value="${item.endDate}" placeholder="例如:2024年6月">
            </div>
          </div>
          <div class="form-group">
            <label for="education-description-${item.id}">描述</label>
            <textarea id="education-description-${item.id}" placeholder="请输入描述">${item.description}</textarea>
          </div>
          <button class="remove-btn" data-id="${item.id}" data-type="education">删除</button>
        `;
        educationList.appendChild(educationItem);
      });
      
      // 添加事件监听
      addRemoveEventListeners();
      addInputEventListeners();
    }

    // 渲染工作经历列表
    function renderExperienceList() {
      experienceList.innerHTML = "";
      experienceItems.forEach((item, index) => {
        const experienceItem = document.createElement("div");
        experienceItem.className = "section-item";
        experienceItem.innerHTML = `
          <h3>工作经历 ${index + 1}</h3>
          <div class="form-group">
            <label for="experience-company-${item.id}">公司</label>
            <input type="text" id="experience-company-${item.id}" value="${item.company}" placeholder="请输入公司名称">
          </div>
          <div class="form-group">
            <label for="experience-position-${item.id}">职位</label>
            <input type="text" id="experience-position-${item.id}" value="${item.position}" placeholder="请输入职位">
          </div>
          <div class="button-group">
            <div class="form-group" style="flex: 1;">
              <label for="experience-start-${item.id}">开始日期</label>
              <input type="text" id="experience-start-${item.id}" value="${item.startDate}" placeholder="例如:2020年9月">
            </div>
            <div class="form-group" style="flex: 1;">
              <label for="experience-end-${item.id}">结束日期</label>
              <input type="text" id="experience-end-${item.id}" value="${item.endDate}" placeholder="例如:2024年6月">
            </div>
          </div>
          <div class="form-group">
            <label for="experience-description-${item.id}">描述</label>
            <textarea id="experience-description-${item.id}" placeholder="请输入描述">${item.description}</textarea>
          </div>
          <button class="remove-btn" data-id="${item.id}" data-type="experience">删除</button>
        `;
        experienceList.appendChild(experienceItem);
      });
      
      // 添加事件监听
      addRemoveEventListeners();
      addInputEventListeners();
    }

    // 添加删除事件监听
    function addRemoveEventListeners() {
      document.querySelectorAll(".remove-btn").forEach(btn => {
        btn.addEventListener("click", function() {
          const id = this.dataset.id;
          const type = this.dataset.type;
          
          if (type === "education") {
            educationItems = educationItems.filter(item => item.id !== id);
            renderEducationList();
          } else if (type === "experience") {
            experienceItems = experienceItems.filter(item => item.id !== id);
            renderExperienceList();
          }
          
          updatePreview();
        });
      });
    }

    // 添加输入事件监听
    function addInputEventListeners() {
      // 教育背景输入
      educationItems.forEach(item => {
        document.getElementById(`education-school-${item.id}`).addEventListener("input", function() {
          item.school = this.value;
          updatePreview();
        });
        document.getElementById(`education-degree-${item.id}`).addEventListener("input", function() {
          item.degree = this.value;
          updatePreview();
        });
        document.getElementById(`education-field-${item.id}`).addEventListener("input", function() {
          item.field = this.value;
          updatePreview();
        });
        document.getElementById(`education-start-${item.id}`).addEventListener("input", function() {
          item.startDate = this.value;
          updatePreview();
        });
        document.getElementById(`education-end-${item.id}`).addEventListener("input", function() {
          item.endDate = this.value;
          updatePreview();
        });
        document.getElementById(`education-description-${item.id}`).addEventListener("input", function() {
          item.description = this.value;
          updatePreview();
        });
      });
      
      // 工作经历输入
      experienceItems.forEach(item => {
        document.getElementById(`experience-company-${item.id}`).addEventListener("input", function() {
          item.company = this.value;
          updatePreview();
        });
        document.getElementById(`experience-position-${item.id}`).addEventListener("input", function() {
          item.position = this.value;
          updatePreview();
        });
        document.getElementById(`experience-start-${item.id}`).addEventListener("input", function() {
          item.startDate = this.value;
          updatePreview();
        });
        document.getElementById(`experience-end-${item.id}`).addEventListener("input", function() {
          item.endDate = this.value;
          updatePreview();
        });
        document.getElementById(`experience-description-${item.id}`).addEventListener("input", function() {
          item.description = this.value;
          updatePreview();
        });
      });
    }

    // 更新预览
    function updatePreview() {
      // 个人信息
      document.querySelector(".resume-name").textContent = nameInput.value || "姓名";
      document.getElementById("previewEmail").textContent = emailInput.value || "邮箱";
      document.getElementById("previewPhone").textContent = phoneInput.value || "电话";
      document.getElementById("previewAddress").textContent = addressInput.value || "地址";
      document.getElementById("previewSummary").textContent = summaryInput.value || "个人简介";
      
      // 技能
      const skills = skillsInput.value.split(",").map(skill => skill.trim()).filter(skill => skill);
      document.getElementById("previewSkills").innerHTML = skills.length > 0 ? 
        skills.map(skill => `<span style="display: inline-block; background-color: #f0f0f0; padding: 5px 10px; border-radius: 15px; margin-right: 10px; margin-bottom: 10px;">${skill}</span>`).join("") : 
        "技能";
      
      // 教育背景
      const educationHtml = educationItems.length > 0 ? 
        educationItems.map(item => `
          <div class="resume-item">
            <div class="resume-item-header">
              <div class="resume-item-title">${item.school}</div>
              <div class="resume-item-date">${item.startDate} - ${item.endDate}</div>
            </div>
            <div class="resume-item-company">${item.degree} | ${item.field}</div>
            <div class="resume-item-description">${item.description}</div>
          </div>
        `).join("") : 
        "教育背景";
      document.getElementById("previewEducation").innerHTML = educationHtml;
      
      // 工作经历
      const experienceHtml = experienceItems.length > 0 ? 
        experienceItems.map(item => `
          <div class="resume-item">
            <div class="resume-item-header">
              <div class="resume-item-title">${item.position}</div>
              <div class="resume-item-date">${item.startDate} - ${item.endDate}</div>
            </div>
            <div class="resume-item-company">${item.company}</div>
            <div class="resume-item-description">${item.description}</div>
          </div>
        `).join("") : 
        "工作经历";
      document.getElementById("previewExperience").innerHTML = experienceHtml;
    }

    // 下载简历
    function downloadResume() {
      // 在实际项目中,可以使用 html2canvas 和 jsPDF 库将简历转换为 PDF
      // 这里只是一个简单的示例
      alert("简历下载功能将在实际项目中实现");
    }

    // 事件监听
    addEducationBtn.addEventListener("click", function() {
      addEducation();
      updatePreview();
    });
    
    addExperienceBtn.addEventListener("click", function() {
      addExperience();
      updatePreview();
    });
    
    nameInput

© 2026 编程马·菜鸟教程 版权所有