关于我的个人博客开发
本项目是一个基于 HTML + CSS + JavaScript 的纯前端个人博客网站,无需后端服务器,所有数据存储在浏览器 localStorage 中。技术栈- HTML5:页面结构- CSS3:样式与布局(Flexbox、Grid)- JavaScript(ES5):交互逻辑、数据存储页面结构(6个页面)
一、项目概述
本项目是一个基于 HTML + CSS + JavaScript 的纯前端个人博客网站,无需后端服务器,所有数据存储在浏览器 localStorage 中。
技术栈
- HTML5:页面结构
- CSS3:样式与布局(Flexbox、Grid)
- JavaScript(ES5):交互逻辑、数据存储
页面结构(6个页面)
- | 文件名 | 功能 |
- | `login.html` |用户注册与登录 |
- | `index.html` | 首页(轮播图 + 内容卡片) |
- | `publish.html` | 发布新文章 |
- | `detail.html` | 文章详情 + 评论区 |
- | `message.html` | 留言板 |
- | `about.html` | 个人介绍页 |
二、核心功能实现
2.1 用户系统(login.html)
实现原理:
- 使用 `localStorage` 存储用户数据
- 用户数据以对象形式存储,key 为 `blog_users`,值为 `{昵称: 密码}` 的 JSON 对象
- 当前登录用户存储在 `blog_current_user` 中
注册逻辑:
javascript
function handleRegister() {
var nickname = document.getElementById('regNickname').value.trim();
var password = document.getElementById('regPassword').value.trim();
// 验证输入...
var users = JSON.parse(localStorage.getItem('blog_users') || '{}');
if (users[nickname]) {
showError('该昵称已被注册!');
return;
}
users[nickname] = password;
localStorage.setItem('blog_users', JSON.stringify(users));
localStorage.setItem('blog_current_user', nickname);
window.location.href = 'index.html';
}
登录逻辑:
javascript
function handleLogin() {
var nickname = document.getElementById('loginNickname').value.trim();
var password = document.getElementById('loginPassword').value.trim();
var users = JSON.parse(localStorage.getItem('blog_users') || '{}');
if (!users[nickname]) { showError('该昵称未注册!'); return; }
if (users[nickname] !== password) { showError('密码错误!'); return; }
localStorage.setItem('blog_current_user', nickname);
window.location.href = 'index.html';
}
登录状态检查:每个页面开头都检查 blog_current_user,确保有登录账号,如果为空则跳转到登录页。
退出登录逻辑:
javascript
function handleLogout() {
if (confirm('确定要退出登录吗?')) {
localStorage.removeItem('blog_current_user');
window.location.href = 'login.html';
}
}
每个页面侧边栏底部都有"退出登录"按钮,点击后弹出确认框,确认后清除当前登录状态并跳转回登录页。
2.2 轮播图(index.html)
实现原理:
- 使用 CSS
flex布局横向排列图片 - 通过
transform: translateX()实现滑动切换 setInterval实现自动播放(每3秒切换)- 圆点指示器与当前图片同步,点击圆点可跳转
HTML 结构:
<div class="carousel">
<div class="carousel-wrap" id="carouselWrap">
<div class="carousel-item"><img src="pic1.jpg"></div>
<div class="carousel-item"><img src="pic2.jpg"></div>
<div class="carousel-item"><img src="pic3.jpg"></div>
<div class="carousel-item"><img src="pic4.jpg"></div>
</div>
<div class="prev">❮</div>
<div class="next">❯</div>
<div class="carousel-dots" id="carouselDots"></div>
</div>
核心 JS 逻辑:
javascript
var current = 0;
var total = 4;
var autoPlay = setInterval(nextSlide, 3000);
function nextSlide() {
current = (current + 1) % total; // 循环到下一张
updateSlide();
}
function prevSlide() {
current = (current - 1 + total) % total; // 循环到上一张
updateSlide();
}
function updateSlide() {
wrap.style.transform = 'translateX(-' + (current * 100) + '%)';
updateDots(); // 同步更新圆点状态
}
圆点指示器:
javascript
// 页面加载时动态创建圆点
var dotsContainer = document.getElementById('carouselDots');
for (var d = 0; d < total; d++) {
var dot = document.createElement('span');
dot.className = 'carousel-dot';
if (d === 0) dot.classList.add('active'); // 第一个默认激活
dot.setAttribute('data-index', d);
dot.onclick = function() {
clearInterval(autoPlay); // 点击时暂停自动播放
current = parseInt(this.getAttribute('data-index'));
updateSlide();
autoPlay = setInterval(nextSlide, 3000); // 重新开始自动播放
};
dotsContainer.appendChild(dot);
}
// 更新圆点激活状态
function updateDots() {
var dots = document.querySelectorAll('.carousel-dot');
for (var d = 0; d < dots.length; d++) {
dots[d].classList.toggle('active', d === current);
}
}
圆点 CSS 样式:
css
.carousel-dots {
position: absolute;
bottom: 15px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
z-index: 10;
}
.carousel-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(255,255,255,0.5);
cursor: pointer;
transition: all 0.3s;
}
.carousel-dot.active {
background: #3182ce; /* 激活时变蓝色 */
width: 24px; /* 激活时变长 */
border-radius: 5px;
}
2.3 内容发布系统(publish.html → index.html)
实现原理:
- 发布页使用表单收集标题、封面选择、内容
- 数据存入
localStorage的blog_posts数组 - 首页通过 JS 动态读取
blog_posts生成新卡片,追加到默认卡片下方
数据结构:
javascript
{
title: '文章标题',
image: 'default' | '' | '自定义路径',
content: '文章内容...',
date: '2025-01-20 15:30',
author: '用户昵称',
comments: []
}
发布逻辑:
javascript
function publishPost() {
var title = document.getElementById('postTitle').value.trim();
var content = document.getElementById('postContent').value.trim();
// 验证输入...
var posts = JSON.parse(localStorage.getItem('blog_posts') || '[]');
var now = new Date();
var dateStr = now.getFullYear() + '-' +
String(now.getMonth()+1).padStart(2,'0') + '-' +
String(now.getDate()).padStart(2,'0') + ' ' +
String(now.getHours()).padStart(2,'0') + ':' +
String(now.getMinutes()).padStart(2,'0');
posts.push({
title: title,
image: selectedCover === 'default' ? 'default' : '',
content: content,
date: dateStr,
author: currentUser,
comments: []
});
localStorage.setItem('blog_posts', JSON.stringify(posts));
alert('🎉 文章已发布成功!');
// 清空表单...
}
首页动态卡片加载:
首页内容卡片加载:
javascript
function loadDynamicCards() {
var container = document.getElementById('cardContainer');
var posts = JSON.parse(localStorage.getItem('blog_posts') || '[]');
// 先清除旧的动态卡片
var oldDynamic = container.querySelectorAll('.dynamic-card');
for (var i = 0; i < oldDynamic.length; i++) {
oldDynamic[i].remove();
}
if (posts.length === 0) return;
// 逐个创建新卡片
for (var j = 0; j < posts.length; j++) {
var post = posts[j];
var card = document.createElement('div');
card.className = 'content-card dynamic-card';
// 根据 image 字段决定封面显示
var imageHtml = '';
if (post.image === 'default') {
// 默认占位图
imageHtml = '<div class="card-img" style="background:linear-gradient(135deg, #e0edff, #c9dff5);display:flex;align-items:center;justify-content:center;font-size:40px;">📄</div>';
} else if (post.image) {
// 有自定义图片
imageHtml = '<img class="card-img" src="' + post.image + '" alt="' + post.title + '">';
} else {
// 无封面
card.classList.add('no-image');
}
card.innerHTML =
'<div class="card-click-area" onclick="location.href=\'detail.html?type=new&id=' + j + '\'">' +
imageHtml +
'<div class="card-text">' +
'<h4>' + escapeHtml(post.title) + '</h4>' +
'<p>' + escapeHtml(post.content) + '</p>' +
'</div>' +
'</div>' +
'<span class="card-delete" data-index="' + j + '" title="删除">🗑️</span>';
container.appendChild(card);
}
// 绑定删除事件
var deleteBtns = document.querySelectorAll('.card-delete');
for (var k = 0; k < deleteBtns.length; k++) {
deleteBtns[k].onclick = function(e) {
e.stopPropagation(); // 阻止触发卡片点击
var index = parseInt(this.getAttribute('data-index'));
if (confirm('确定要删除这篇文章吗?')) {
var posts = JSON.parse(localStorage.getItem('blog_posts') || '[]');
posts.splice(index, 1); // 从数组中移除
localStorage.setItem('blog_posts', JSON.stringify(posts));
loadDynamicCards(); // 重新渲染
}
};
}
}
封面选择逻辑:
selectedCover = 'none':无封面,卡片纯文本显示selectedCover = 'default':默认封面,显示占位图- 通过点击按钮切换
selectedCover变量,发布时写入image字段
2.4 文章详情与评论(detail.html)
实现原理:
- 通过 URL 参数区分文章类型
?type=default&id=0:默认文章(写死在 JS 数组中)?type=new&id=0:用户发布的文章(从 localStorage 读取)- 评论按文章 ID 独立存储
文章加载:
javascript
function loadArticle() {
var article = null;
if (type === 'default' && id >= 0 && id < 6) {
// 6篇默认文章
article = defaultArticles[id];
currentStorageKey = 'default_comments_' + id;
} else if (type === 'new') {
var posts = JSON.parse(localStorage.getItem('blog_posts') || '[]');
if (id >= 0 && id < posts.length) {
article = posts[id];
currentStorageKey = 'new_comments_' + id;
}
}
// 渲染标题、日期、内容...
}
评论系统:
javascript
function addComment() {
var input = document.getElementById('commentInput');
var text = input.value.trim();
if (!text) { alert('请输入评论内容!'); return; }
var now = new Date();
var timeStr = now.getFullYear() + '-' +
String(now.getMonth()+1).padStart(2,'0') + '-' +
String(now.getDate()).padStart(2,'0') + ' ' +
String(now.getHours()).padStart(2,'0') + ':' +
String(now.getMinutes()).padStart(2,'0');
currentComments.push({
user: currentUser, // 当前登录用户昵称
text: text,
time: timeStr
});
localStorage.setItem(currentStorageKey, JSON.stringify(currentComments));
input.value = '';
renderComments(); // 重新渲染评论列表
}
默认文章数据:6篇默认文章展示样式,包含标题、内容、日期、作者、预设评论。
2.5 留言板(message.html)
实现原理:
- 数据存储在
blog_messages数组中 - 首次加载时通过
blog_messages_initialized标记判断,写入两条默认留言 - 所有留言按时间倒序排列(最新在上面)
- 只有当前用户自己的留言才显示删除按钮
数据结构:
javascript
{ user: '用户昵称', text: '留言内容', time: '2026-05-20 15:30' }
加载留言:
javascript
function loadMessages() {
var messages = JSON.parse(localStorage.getItem('blog_messages') || '[]');
// 首次加载写入默认留言
var hasDefault = localStorage.getItem('blog_messages_initialized');
if (!hasDefault) {
var defaultMessages = [
{ user: '小明', text: '博主加油!网站做得很棒 👍', time: '2026-03-20 15:30' },
{ user: '小红', text: '来踩踩,互相学习~', time: '2025-02-21 09:15' }
];
messages = defaultMessages.concat(messages);
localStorage.setItem('blog_messages', JSON.stringify(messages));
localStorage.setItem('blog_messages_initialized', 'true');
}
// 倒序渲染...
}
删除判断:
javascript
var canDelete = (m.user === currentUser) ?
'<span class="msg-delete" onclick="deleteMessage(' + i + ', event)">🗑️</span>' : '';
2.6 个人介绍页(about.html)
实现原理:
- 个人信息存储在
blog_about对象中 - 各字段(昵称、性别、签名、爱好)独立编辑,通过切换 display 实现编辑/显示模式
- 下方学习笔记卡片存储在
blog_about_cards数组中 - 首次加载默认生成"学习笔记1"和"学习笔记2"
编辑模式切换:
javascript
function editField(key) {
// 隐藏显示区,显示编辑区
document.getElementById('display' + key).style.display = 'none';
document.getElementById('edit' + key).style.display = 'flex';
document.getElementById('input' + key).focus();
}
function saveField(key) {
var val = document.getElementById('input' + key).value.trim();
var about = JSON.parse(localStorage.getItem('blog_about') || '{}');
about[key] = val;
localStorage.setItem('blog_about', JSON.stringify(about));
loadAboutInfo(); // 刷新显示
}
三、数据存储方案
| Key | 数据类型 | 说明 |
|---|---|---|
blog_users |
Object | 用户账号密码,格式:{昵称: 密码} |
blog_current_user |
String | 当前登录用户昵称 |
blog_posts |
Array | 用户发布的文章列表 |
blog_messages |
Array | 留言板数据 |
blog_messages_initialized |
String | 留言板是否已初始化 |
blog_about |
Object | 个人介绍信息 |
blog_about_cards |
Array | 关于页自定义卡片 |
blog_about_cards_init |
String | 关于页卡片是否已初始化 |
default_comments_0 ~ default_comments_5 |
Array | 6篇默认文章的评论 |
new_comments_0 ~ new_comments_N |
Array | 发布文章的评论 |
四、布局与样式设计
4.1 整体布局
- 左侧固定导航栏(240px 宽度)+ 右侧内容区(flex: 1 自适应)
- 左侧导航栏使用
position: fixed固定定位 - 右侧内容区
margin-left: 240px避开导航栏
4.2 卡片网格
- 使用 CSS Grid 布局:
grid-template-columns: repeat(3, 1fr) - 每行固定3个卡片,新增卡片自动换行到下一行
- 卡片间距 30px
4.3 响应式设计
@media (max-width: 900px)改为单列布局- 导航栏变为横向排列
- 卡片网格改为
grid-template-columns: 1fr
4.4 全局背景
body设置background-image: url('bg.jpg')background-size: cover覆盖整个页面background-attachment: fixed背景固定不随滚动
五、项目亮点
- 纯前端实现:无需后端服务器,所有功能通过 localStorage 实现。
- 数据持久化:刷新页面数据不丢失,关闭浏览器重新打开依然保留。
- 用户系统:支持注册和登录,多用户数据隔离。
- 文章管理:发布、展示、评论、删除全流程闭环。
- 留言板:支持用户留言,按时间倒序展示。
- 轮播图:自动播放 + 手动切换 + 圆点指示器。
- 个人主页:关于我页面支持自定义编辑个人信息和学习笔记。
- 封面选择:发布时可选无封面或默认封面,灵活展示。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)