Vue 与 ElementUI 实现分页、HTTP 封装及农历转换的工程实践
在构建现代前端应用时,我们常常需要处理大量数据展示、统一管理接口请求,并增强用户对时间信息的理解。尤其是在政务系统、日程提醒、文化类平台等场景中,不仅要呈现公历日期,还需标注农历节日和节气,以提升本土化体验。
本文将围绕一个真实可用的技术栈组合——Vue 2 + ElementUI,深入探讨如何在一个项目中高效整合三大核心功能:
- 基于el-pagination的智能分页展示
- 使用 Axios 封装的可复用 HTTP 请求模块
- 支持节日识别的公历转农历工具
整个实现过程注重代码结构清晰、逻辑解耦与工程可维护性,适合用于中后台管理系统或内容型 Web 应用的快速开发。
当我们面对一个包含数百条文章的数据列表时,直接渲染显然不现实。更合理的做法是通过分页控制每页显示数量,既减轻浏览器负担,也提升用户体验。
ElementUI 提供了开箱即用的<el-pagination>组件,配合 Vue 的计算属性(computed),我们可以轻松实现本地分页:
<template> <div class="article-page"> <h2>技术博客列表</h2> <ul class="article-list"> <li v-for="(item, index) in paginatedList" :key="index" class="article-item"> <h3>{{ item.title }}</h3> <p>{{ item.summary }}</p> <small>发布于:{{ item.date }}</small> </li> </ul> <div class="pagination-container"> <el-pagination @current-change="handlePageChange" :current-page="currentPage" :page-size="pageSize" :total="articleList.length" layout="prev, pager, next, total" background /> </div> </div> </template> <script> export default { name: 'ArticleList', data() { return { currentPage: 1, pageSize: 8, articleList: [] } }, computed: { paginatedList() { const start = (this.currentPage - 1) * this.pageSize; const end = start + this.pageSize; return this.articleList.slice(start, end); } }, methods: { handlePageChange(page) { this.currentPage = page; } }, mounted() { this.fetchArticles(); } } </script>这里的paginatedList是典型的计算属性用法:它依赖currentPage和pageSize自动响应式更新,无需手动触发重计算。当用户点击页码时,@current-change回调更新当前页,视图随之刷新。
值得注意的是,这种模式适用于前端已有完整数据的情况。若后端支持分页查询,建议改为传递page参数到 API,减少网络传输量。
为了统一管理所有 API 调用,避免重复编写请求头、错误处理逻辑,封装一个全局 HTTP 工具是必要的工程实践。
我们基于 Axios 创建src/utils/request.js,并加入拦截器机制,实现权限认证与异常统一处理:
import axios from 'axios' const service = axios.create({ baseURL: process.env.NODE_ENV === 'production' ? 'https://api.example.com' : '/api', timeout: 10000, headers: { 'Content-Type': 'application/json' } }) // 请求拦截:添加 token service.interceptors.request.use( config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = `Bearer ${token}` } return config }, error => { console.error('Request Error:', error) return Promise.reject(error) } ) // 响应拦截:统一错误提示 service.interceptors.response.use( response => { const res = response.data if (res.code !== 200 && res.code !== undefined) { alert(`Error: ${res.message || '未知错误'}`) return Promise.reject(new Error(res.message || 'Error')) } return res }, error => { if (error.response) { switch (error.response.status) { case 401: alert('未授权,请登录') break case 404: alert('请求资源不存在') break default: alert('网络异常') } } return Promise.reject(error) } ) export default service这个封装带来的好处非常明显:
- 环境适配:根据
NODE_ENV切换基础 URL,开发调试更方便; - 安全加固:自动携带 Token,防止未授权访问;
- 错误集中处理:无论是业务码非 200 还是 HTTP 状态码异常,都能及时反馈给用户;
- 易于扩展:后续可加入 loading 拦截、缓存策略、重试机制等。
在组件中使用也非常简洁:
import request from '@/utils/request' export default { methods: { async fetchArticles() { try { const res = await request({ url: '/articles', method: 'get' }) this.articleList = res.data || [] } catch (err) { this.articleList = [] } } } }一行调用即可完成带拦截、鉴权、错误处理的请求,极大提升了开发效率。
传统节日和二十四节气在中国文化中具有重要意义。在日历类应用中仅显示公历显然不够友好。为此,我们需要将公历日期转换为农历,并标注节日与节气。
我们创建src/utils/calendar.js,引入轻量级农历算法库(适用于 1900–2100 年):
const calendar = { lunarInfo: [ 0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2, // ... 中间省略,实际项目请补全全部数据 ], solarTerm: [ "小寒","大寒","立春","雨水","惊蛰","春分","清明","谷雨", "立夏","小满","芒种","夏至","小暑","大暑","立秋","处暑", "白露","秋分","寒露","霜降","立冬","小雪","大雪","冬至" ], festival: { "1-1": { title: "元旦" }, "5-1": { title: "劳动节" }, "10-1": { title: "国庆节" }, "12-25": { title: "圣诞节" } }, lFestival: { "1-1": { title: "春节" }, "1-15": { title: "元宵节" }, "5-5": { title: "端午节" }, "8-15": { title: "中秋节" }, "12-8": { title: "腊八节" } }, solar2lunar(solarYear, solarMonth, solarDay) { // 此处为简化示意,实际需完整实现 offset 计算、闰月判断等 const festivals = this.festival[`${solarMonth}-${solarDay}`] return { IDayCn: `${solarDay}`, IMonthCn: `${solarMonth}月`, festival: festivals ? festivals.title : null, Term: null, isLeap: false, Animal: ['鼠','牛','虎','兔','龙','蛇','马','羊','猴','鸡','狗','猪'][(solarYear - 4) % 12] } } } export default calendar⚠️ 注意:完整版
lunarInfo数组包含约 250 项,用于精确计算每年的农历月份天数与闰月位置。建议从开源项目如 jjonline/calendar 获取完整算法。
在 Vue 组件中集成该工具,结合el-calendar自定义单元格内容:
<template> <el-calendar v-model="currentDate"> <template #date-cell="{data}"> <div @click="onDateClick(data)"> <span class="day">{{ data.day.split('-')[2] }}</span> <div class="lunar-info" :class="{ 'holiday': isHoliday(data) }"> {{ getLunarText(data) }} </div> </div> </template> </el-calendar> </template> <script> import calendar from '@/utils/calendar' export default { data() { return { currentDate: new Date() } }, methods: { onDateClick(data) { console.log('Clicked date:', data.day) }, getLunarText(data) { const [y, m, d] = data.day.split('-').map(Number) const lunar = calendar.solar2lunar(y, m, d) if (lunar.festival) return lunar.festival if (lunar.Term) return lunar.Term return lunar.IMonthCn + lunar.IDayCn }, isHoliday(data) { const [y, m, d] = data.day.split('-').map(Number) const lunar = calendar.solar2lunar(y, m, d) return !!(lunar.festival || lunar.Term) } } } </script> <style scoped> .el-calendar ::v-deep .el-calendar-day { height: 100px; display: flex; flex-direction: column; justify-content: center; align-items: center; } .day { font-size: 14px; font-weight: bold; } .lunar-info { font-size: 11px; color: #999; margin-top: 4px; } .holiday { color: #f56c6c !important; font-weight: bold; } </style>效果上,每一天都会显示对应的农历信息,节日或节气则以红色高亮,视觉区分明显,用户一眼即可识别重要日子。
最终,我们将三个模块融合成一个完整的仪表盘页面,左侧展示分页文章列表,右侧嵌入农历日历:
<!-- ArticleWithCalendar.vue --> <template> <div class="dashboard"> <h1>技术文章与日历系统</h1> <div class="content-area"> <!-- 左侧文章列表 --> <section class="articles"> <h2>最新文章</h2> <ul class="article-list"> <li v-for="(art, idx) in paginatedList" :key="idx"> <router-link :to="'/article/' + art.id"> {{ art.title }} <em>{{ art.date }}</em> </router-link> </li> </ul> <el-pagination @current-change="handlePageChange" :current-page="currentPage" :page-size="8" :total="articleList.length" layout="prev, pager, next, total" /> </section> <!-- 右侧日历 --> <section class="calendar-section"> <el-calendar v-model="today"> <template #date-cell="{data}"> <div> <span>{{ data.day.split('-')[2] }}</span> <div class="lunar-tip" :class="{fest: isFestival(data)}"> {{ lunarLabel(data) }} </div> </div> </template> </el-calendar> </section> </div> </div> </template> <script> import request from '@/utils/request' import calendar from '@/utils/calendar' export default { data() { return { currentPage: 1, articleList: [], today: new Date() } }, computed: { paginatedList() { const start = (this.currentPage - 1) * 8 return this.articleList.slice(start, start + 8) } }, methods: { handlePageChange(page) { this.currentPage = page }, async fetchArticles() { try { const res = await request.get('/articles') this.articleList = res.data.map(d => ({ id: d.id, title: d.title, date: d.pub_date })) } catch (e) { this.articleList = [] } }, lunarLabel(data) { const [y, m, d] = data.day.split('-').map(Number) const lunar = calendar.solar2lunar(y, m, d) if (lunar.festival) return lunar.festival if (lunar.Term) return lunar.Term return lunar.IMonthCn + lunar.IDayCn }, isFestival(data) { const [y, m, d] = data.day.split('-').map(Number) const lunar = calendar.solar2lunar(y, m, d) return !!lunar.festival || !!lunar.Term } }, mounted() { this.fetchArticles() } } </script> <style scoped> .dashboard { max-width: 1200px; margin: 0 auto; padding: 20px; } .content-area { display: flex; gap: 30px; } .articles { flex: 1; } .calendar-section { width: 300px; } .article-list { list-style: none; padding: 0; } .article-list li { padding: 8px 0; border-bottom: 1px solid #eee; } .lunar-tip { font-size: 12px; color: #aaa; } .fest { color: #e60000; font-weight: bold; } </style>这样的布局兼顾功能性与美观性,特别适合资讯门户、企业内网、个人博客后台等场景。
这套方案的价值不仅在于实现了具体功能,更在于其体现的工程化思维:
- 分页设计避免一次性加载过多数据,保障性能;
- HTTP 封装实现请求标准化,降低维护成本;
- 农历工具提升产品文化感知力,增强用户体验。
这些模式均可独立抽离为公共模块,在多个项目中复用。例如,可将calendar打包为 NPM 包,或将请求拦截器抽象为插件形式。
未来还可进一步优化:
- 引入 Vuex 或 Pinia 管理全局状态(如当前页、用户信息);
- 使用 TypeScript 增强类型安全;
- 接入真实农历 API 提供更高精度服务;
- 支持主题切换与国际化。
前端开发不仅是“写页面”,更是构建可持续演进的系统。从一个小功能出发,思考其背后的架构意义,才能真正写出高质量、易维护的代码。