Appearance
综合实战项目(完整功能)
21.1 学生信息管理系统
项目介绍
学生信息管理系统是一个用于管理学生信息的应用,可以添加、编辑、删除和查询学生信息。
实现思路
- 设计系统的 UI 界面
- 实现学生信息的添加功能
- 实现学生信息的编辑功能
- 实现学生信息的删除功能
- 实现学生信息的查询功能
- 使用本地存储保存学生数据
完整代码
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">×</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 响应式购物车
项目介绍
响应式购物车可以在不同设备上正常显示,用户可以添加商品到购物车、修改商品数量和删除商品。
实现思路
- 设计响应式的购物车界面
- 实现商品的添加功能
- 实现商品数量的修改功能
- 实现商品的删除功能
- 实现购物车总价的计算
- 使用本地存储保存购物车数据
完整代码
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 天气查询小工具
项目介绍
天气查询小工具可以根据用户输入的城市名称查询天气信息,并显示当前天气和未来几天的天气预报。
实现思路
- 设计天气查询的 UI 界面
- 实现城市名称的输入和提交
- 使用天气 API 获取天气数据
- 显示当前天气信息
- 显示未来几天的天气预报
- 处理错误情况
完整代码
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 个人简历生成器
项目介绍
个人简历生成器可以帮助用户创建和下载个人简历,用户可以输入个人信息、教育背景、工作经历等内容。
实现思路
- 设计简历生成器的 UI 界面
- 实现个人信息的输入功能
- 实现教育背景的添加和删除功能
- 实现工作经历的添加和删除功能
- 实现简历的预览功能
- 实现简历的下载功能
完整代码
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