HTML+CSS+JS对话式实战(6)-学生端查看活动功能开发

点此查看 所有教程、项目、源码导航

文章目录

  • [1. 本篇任务说明](#1. 本篇任务说明)
  • [2. 接口约定](#2. 接口约定)
  • [3. HTML 代码](#3. HTML 代码)
  • [4. activity-student.css](#4. activity-student.css)
  • [5. activity-student.js](#5. activity-student.js)
  • [6. 活动详情弹窗CSS美化](#6. 活动详情弹窗CSS美化)
  • [7. 小结](#7. 小结)

1. 本篇任务说明

毛哥:副队,管理员这边的功能我们已经做完了,接下来该做学生端了对吧?

副队:没错,这一篇我们先来实现学生查看活动的页面,包含静态布局,再对接接口把活动列表拉出来渲染到页面上。

毛哥:学生端页面叫 activity-student.html,样式文件是 activity-student.css,公共样式继续共用之前的,对吧?

副队:对,公共样式还是单独一份,页面独有的样式放到 activity-student.css,弹窗依旧沿用 mask、dialog 这套结构,保持整个项目风格统一。

毛哥:那获取活动的接口也是 POST 请求吗?

副队:是的,咱们项目约定全部使用 POST,接口地址是 /activity/list,后端依旧返回 {code, msg, data} 统一格式。

毛哥:明白了,拿到数据之后渲染列表,点击每一条活动,还能弹出弹窗查看活动完整详情。

2. 接口约定

副队:先把这个查询活动的接口信息整理出来。

项目 内容
请求方式 POST
请求地址 /activity/list
请求参数 无
返回格式 {code, msg, data}
data 活动对象数组,单个活动包含:id、name、type、time、address、endTime、maxNum、about

示例返回 JSON:

js 复制代码
{
  "code": 200,
  "msg": "查询成功",
  "data": [
    {
      "id": 1,
      "name": "春日户外拓展活动",
      "type": "户外",
      "time": "2026-11-15 09:00",
      "address": "学校后山广场",
      "endTime": "2026-11-12 23:59",
      "maxNum": 50,
      "about": "面向全体在校学生的户外拓展活动,鼓励大家走出教室锻炼身体。"
    },
    {
      "id": 2,
      "name": "编程分享会",
      "type": "学习",
      "time": "2026-11-20 14:00",
      "address": "计算机学院 203 教室",
      "endTime": "2026-11-18 23:59",
      "maxNum": 30,
      "about": "由学长分享前端、后端入门经验,适合零基础同学参加。"
    }
  ]
}

毛哥:如果暂时还没有活动呢?

副队:后端正常返回 code 为 200,data 是空数组,我们前端判断,如果数组为空,就在页面上提示暂无活动。

3. HTML 代码

副队:我们新建 activity-student.html,在头部依次引入公共 CSS,再引入页面专属的 activity-student.css。页面包含导航、活动列表区域,还有查看详情的弹窗。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>活动中心 - 学生端</title>
  <!-- 公共样式在前 -->
  <link rel="stylesheet" href="common.css">
  <!-- 当前页面专属样式 -->
  <link rel="stylesheet" href="activity-student.css">
</head>
<body>
  <!-- 导航栏 -->
  <div class="nav">
    <div class="nav-left">
       <a href="activity-student.html">学生活动中心</a>
    </div>
    <div class="nav-right">
      <span id="studentInfo">欢迎,同学</span>
    </div>
  </div>

  <!-- 主容器 -->
  <div class="container">
    <h3 class="title">可报名活动列表</h3>
    <div id="emptyTip" class="empty-tip" style="display: none;">暂时没有活动</div>
    <div class="activity-list" id="activityList">
      <!-- JS 动态渲染活动卡片 -->
    </div>
  </div>

  <!-- 查看活动详情弹窗 -->
  <div class="mask" id="detailMask">
    <div class="dialog">
      <h3 id="detailTitle">活动详情</h3>
      <div class="dialog-item">
        <label>活动名称</label>
        <span id="act-name"></span>
      </div>
      <div class="dialog-item">
        <label>分类</label>
        <span id="act-type"></span>
      </div>
      <div class="dialog-item">
        <label>活动时间</label>
        <span id="act-time"></span>
      </div>
      <div class="dialog-item">
        <label>活动地点</label>
        <span id="act-address"></span>
      </div>
      <div class="dialog-item">
        <label>报名截止</label>
        <span id="act-endTime"></span>
      </div>
      <div class="dialog-item">
        <label>人数上限</label>
        <span id="act-maxNum"></span>
      </div>
      <div class="dialog-item">
        <label>简介</label>
        <span id="act-about"></span>
      </div>
      <div class="dialog-footer">
        <button class="close" onclick="closeDetailMask()">关闭</button>
      </div>
    </div>
  </div>

  <!-- 通用请求脚本 -->
  <script src="common.js"></script>
  <!-- 当前页面业务脚本 -->
  <script src="activity-student.js"></script>
</body>
</html>

毛哥:这里详情弹窗也是 mask 和 dialog,和管理员那边完全一样对吧?

副队:没错,mask、dialog、dialog-item、dialog-footer 全部都在 common.css,我们只需要写卡片布局这类学生页面独有的样式,放到 activity-student.css。

毛哥:为何活动详情里面的字段要加act-这个前缀呢?

副队:其实此处不加也行,因为当前页面只有活动这个概念。但是如果你开发大型项目,一个页面上有很多概念的时候,可以通过前缀区分是哪个概念下面的字段。

4. activity-student.css

副队:接下来编写页面专属 CSS,每一块我都加上注释,方便看懂。

css 复制代码
/*
 * activity-student.css
 * 学生端页面独有样式
 */

/* 页面主标题 */
.title {
  font-size: 22px;
  margin-bottom: 20px;
  font-weight: 600;
}

/* 无活动提示 */
.empty-tip {
  padding: 40px;
  text-align: center;
  color: #666;
  font-size: 16px;
}

/* 活动列表容器,使用弹性布局,自动换行 */
.activity-list {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
}

/* 单张活动卡片 */
.activity-card {
  width: 280px;
  border: 1px solid #e8e8e8;
  border-radius: 8px;
  padding: 16px;
  box-sizing: border-box;
  transition: all 0.2s;
  cursor: pointer;
}

/* 鼠标悬浮卡片效果 */
.activity-card:hover {
  border-color: #4096ff;
  box-shadow: 0 2px 8px rgba(64, 150, 255, 0.15);
}

/* 卡片标题 */
.activity-card h4 {
  margin: 0 0 10px;
  font-size: 18px;
}

/* 卡片内每一行信息 */
.activity-card p {
  margin: 6px 0;
  font-size: 14px;
  color: #555;
}

5. activity-student.js

副队:JS 部分,我们写三个函数:页面加载自动拉取活动列表、渲染卡片、打开和关闭详情弹窗,继续使用我们封装好的 postRequest。

js 复制代码
/**
 * 页面加载,自动获取活动列表
 */
function loadActivityList() {
  postRequest("/activity/list", {}, function (res) {
    if (res.code === 200) {
      renderActivityList(res.data);
    } else {
      alert(res.msg);
    }
  })
}

/**
 * 渲染活动卡片列表
 * @param {Array} list 活动数组
 */
function renderActivityList(list) {
  const listDom = document.getElementById("activityList");
  const emptyDom = document.getElementById("emptyTip");
  listDom.innerHTML = "";

  if (list.length === 0) {
    emptyDom.style.display = "block";
    return;
  }
  emptyDom.style.display = "none";

  list.forEach(function (item) {
    const card = document.createElement("div");
    card.className = "activity-card";
    card.innerText = "";
    card.innerHTML = `
      <h4>${item.name}</h4>
      <p>分类:${item.type}</p>
      <p>活动时间:${item.time}</p>
      <p>地点:${item.address}</p>
      <p>报名截止:${item.endTime}</p>
    `;
    // 点击卡片打开详情
    card.onclick = function () {
      openDetailMask(item);
    };
    listDom.appendChild(card);
  })
}

/**
 * 打开详情弹窗,回填数据
 * @param {Object} act 活动对象
 */
function openDetailMask(act) {
  document.getElementById("detailMask").classList.add("show");

  document.getElementById("act-name").innerText = act.name;
  document.getElementById("act-type").innerText = act.type;
  document.getElementById("act-time").innerText = act.time;
  document.getElementById("act-address").innerText = act.address;
  document.getElementById("act-endTime").innerText = act.endTime;
  document.getElementById("act-maxNum").innerText = act.maxNum;
  document.getElementById("act-about").innerText = act.about;
}

/**
 * 关闭详情弹窗
 */
function closeDetailMask() {
  document.getElementById("detailMask").classList.remove("show");
}

// 页面加载完成,立刻加载活动列表
window.onload = function () {
  loadActivityList();
}

毛哥:点击卡片就能加载全部信息,这个写法很舒服。

副队:对,列表接口一次性返回全部字段,就不用额外再调一次详情接口。

打开学生端,能以卡片形式看到所有当前活动:

点击具体的活动,可以弹窗看到详情:

6. 活动详情弹窗CSS美化

毛哥:副队,这弹窗有点不好看啊。

副队:前端你是专业的,你来搞。

毛哥:No Problem。

先给活动弹窗加个样式类view-mask:

html 复制代码
<div class="mask view-mask" id="detailMask">

CSS代码修改如下:

css 复制代码
.view-mask .dialog {
  width: 540px;
  max-width: 92%;
  background: #ffffff;
  border-radius: 12px;
  padding: 30px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.16);
  box-sizing: border-box;
}

.view-mask .dialog > h3 {
  margin: 0 0 22px;
  font-size: 22px;
  color: #222;
  text-align: center;
  padding-bottom: 14px;
  border-bottom: 1px solid #eeeeee;
}

.view-mask .dialog-item {
  display: flex;
  margin-bottom: 16px;
  line-height: 1.7;
}

.view-mask .dialog-item label {
  width: 96px;
  flex-shrink: 0;
  font-weight: 600;
  color: #303030;
}

.view-mask .dialog-item span {
  flex: 1;
  color: #555555;
  word-break: break-word;
}

.view-mask .dialog-footer {
  margin-top: 28px;
  text-align: center;
}

.view-mask .dialog-footer .close {
  padding: 9px 26px;
  border: none;
  background-color: #4096ff;
  color: #fff;
  border-radius: 6px;
  cursor: pointer;
  font-size: 15px;
  transition: background-color 0.2s ease;
}

.view-mask .dialog-footer .close:hover {
  background-color: #2b7edb;
}

再次打开,果然不错了:

7. 小结

副队:写完之后,我们按下面流程测试。

  1. 打开 activity-student.html;
  2. 页面加载,自动调用 /activity/list;
  3. 如果有活动,生成卡片展示;没有活动,展示提示文字;
  4. 点击任意活动卡片,弹出详情弹窗,所有信息正常展示;
  5. 点击关闭按钮,弹窗消失;
  6. 如果接口返回失败,弹出对应的提示信息。
    毛哥:静态布局、样式、拉取列表、查看详情全部搞定!那下一篇我们就接着在这个页面加上报名弹窗和提交报名的逻辑?

副队:没错,下一篇继续在这个页面新增报名表单弹窗,对接报名接口。

相关推荐
code2cat1 小时前
HTML基础篇之第一张网页:从index.html到浏览器里的任务清单
前端·html
子兮曰19 小时前
Chrome 权限元素实战:144 与 151 的渐进增强写法
前端·javascript·chrome
强尼银手19 小时前
我花一天做了个格斗游戏原型,判定全对,然后被「美术」劝退了
前端·javascript
总要学点什么21 小时前
第98篇:三维特效-材质线效果——给一条线换十种材质,虚线的图案只填了一半
开发语言·前端·javascript·ecmascript·gis·webgl·材质
OpsEye21 小时前
混合云 AI 架构,如何统一做敏感内容拦截?
javascript·ai编程
Funlang1 天前
给机器写的 JavaScript 造个笼子
开发语言·javascript·ecmascript
古茗前端团队1 天前
由 key 引发的思考
前端·javascript·react.js
kyriewen1 天前
我扫了 422 个仓库,近一半 CI 在裸装依赖
前端·javascript·程序员
用户298698530141 天前
Python 实战:将 HTML 批量转换为 Markdown
python·html·markdown