一、项目概述

本项目是一个基于 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 背景固定不随滚动

五、项目亮点

  1. 纯前端实现:无需后端服务器,所有功能通过 localStorage 实现。
  2. 数据持久化:刷新页面数据不丢失,关闭浏览器重新打开依然保留。
  3. 用户系统:支持注册和登录,多用户数据隔离。
  4. 文章管理:发布、展示、评论、删除全流程闭环。
  5. 留言板:支持用户留言,按时间倒序展示。
  6. 轮播图:自动播放 + 手动切换 + 圆点指示器。
  7. 个人主页:关于我页面支持自定义编辑个人信息和学习笔记。
  8. 封面选择:发布时可选无封面或默认封面,灵活展示。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐