用Vue实现一个简易记账本

1,418 阅读4分钟

初学 Vue 和 TypeScript,决定做一个简易的记账本来练手。

从自身需求出发,这个小项目实现以下几点功能:

  1. 记账:可以快速记录一笔账单的消费/收入来源(标签),选择这笔账单的时间,以及记录金额。
  2. 标签管理:可以对修改已有标签的名字,或者自定义新的标签。
  3. 查看账单:可以逐条展示所有的账单记录,也可以根据年份和月份来筛选出记录。同时添加统计图可以更直观地显示记录。 基于上述功能,共设计记账页(主页)、标签管理页、删除标签页、统计页四个页面。

本项目用 Vue Cli 搭建,用到了 VueVue RouterVuexTypeScriptSass 技术。

链接:点击预览 Github源代码

一些思路和总结

1. 添加底部导航,实现页面跳转

首先确定每个页面的 url ,然后添加 Vue Router。每个页面对应各自的组件,用 redirect 重定向实现进入默认页面。

const routes: Array<RouteConfig> = [
  {
    path: '/',
    redirect: '/money'
  },
  {
    path: '/money',
    component: Money
  },
  {
   path: '/labels',
   component: Labels
  },
  {
    
    path: '/labels/edit/:type/:id',  //传递参数 type 和 id
    component: EditLabel
  },
  {
    path:'/statistics',
    component: Statistics
  },
  {
    path: '*',
    component: NotFound
  }
]

const router = new VueRouter({
  routes
})

在导航栏组件中添加 router-link,并且添加 active-class 属性,设置链接激活时使用的 CSS 类名。以此来添加样式。

<nav>
  <router-link to="/labels" class="item" active-class="selected">
    <Icon name="tag" class="icon"/>
    标签
  </router-link>
  <router-link to="/money" class="item" active-class="selected">
    <Icon name="money"/>
    记一笔
  </router-link>
  <router-link to="/statistics" class="item" active-class="selected">
    <Icon name="statistics"/>
    统计
  </router-link>
</nav>

2. 引入 svg

从 iconfont 下载所需的 svg 图标。按照教程添加如下代码:

  1. 添加 HTML 代码
<svg class="icon" aria-hidden="true"> 
    <use xlink:href="#icon-xxx"></use> 
</svg>
  1. 添加样式
<style type="text/css">
.icon { 
    width: 1em; 
    height: 1em; 
    vertical-align: -0.15em; 
    fill: currentColor; 
    overflow: hidden; 
} 
</style>
  1. 引入
 import '@/assets/icons/xxx.svg'

此时还是不能显示 svg, 发现是缺 svg-sprite-loader。解决办法:

  1. 安装 svg-sprite-loader
yarn add svg-sprite-loader --dev (或者使用 npm)
  1. 配置 vue.config.js
const dir = path.resolve(__dirname, 'src/assets/icons') // icons 目录

config.module
  .rule('svg-sprite')
  .test(/.svg$/)
  .include.add(dir).end() // 包含 icons 目录
  .use('svg-sprite-loader').loader('svg-sprite-loader').options({extract:false}).end()
config.plugin('svg-sprite').use(require('svg-sprite-loader/plugin'), [{plainSprite: true}])
config.module.rule('svg').exclude.add(dir) // 其他 svg loader 排除 icons 目录

以上的设置就可以顺利引入 svg 了。

  1. 但是在使用的过程中,会遇到 svg 的一个坑是:发现在 scss 中设置了 svg 的颜色后,依然无法生效。 首先确保样式没有写错,检查 svg 源文件后,发现有些 svg 自带 fill 属性,这个属性使得图标拥有一个默认的颜色且不可被更改。

有一个简单粗暴地方法是,把所有 svg 地 fill 属性手动移除, 就可以给 svg 添加自定义颜色了。 但是问题来了,如果项目中引入几百个图标,总不能靠手动一一删除,不太现实。所以还是要找一个更优的方案。最好可以在引入时就自动删除 svg 的 fill 属性。

经过我的一番搜索,终于发现了一个可行方案!解决方法如下:

首先下载 svgo-loader

yarn add svgo-loader --dev (或者使用npm)

vue.config.js 中添加以下两行:

.use('svgo-loader').loader('svgo-loader')
.tap(options => ({...options, plugins: [{removeAttrs: {attrs: 'fill'}}]})).end()

好了,这样就借助svgo-loader 自动删除所有引入的 svg 的 fill 属性了!

3. 关于整体布局

为了适配各种大小的手机页面,根据我的设计稿,采用一种有头部和底部导航栏,且上下固定中间可滑动显示的布局。所以用 Flex 布局来实现。 思路如下:

  1. 设置 body 的高度为 100vh;
  2. 给最外层 div 添加样式
display: flex;
flex-direction: column;
height: 100%;
  1. 给中间部分添加样式
flex:1; 
overflow: auto;

效果如下:

动画.gif

4. 利用 localStorage 进行数据的存储

因为这只是一个本地版的 app,将数据存储到 localStorage 是一个比较好的选择。我用到的仅仅是 localStorage 存储数据和保存数据的用法。通过 window.localStorage 即可访问到。

localStorage.getItem("name");  //读取保存在localStorage对象里名为name的变量的值
localStorage.setItem("data", data); // 存储名字为name值为 data 的变量

localStorage 的更多用法可以参考 localStorage用法小总结

5. 每个页面的逻辑实现

  • 记账页 作为最主要的功能,所以这个页面也是这个 app 的主页。为了更好地模块化,将这一页面又分为不同的组件,分别负责一条记录的标签、类型(支出或收入)、备注、金额输入的数据逻辑以及样式处理。

父子组件之间数据的传递方式为:

  1. 父组件可以使用 props 把数据传给子组件。
  2. 子组件可以使用 $emit 触发父组件的自定义事件。 这样,在记账页面可以生成一条完整的账单记录。
  • 标签管理页 读取 localStorage 中所有的标签并且显示出来。其中每一个标签可以点进进入标签编辑页,所以需要设置每一个标签为 router-link 传递 typeid(唯一标识每一个标签) 参数。
  • 标签编辑页 通过router-link传递来的typeid 可以从 localStorage 中找到对应的标签对其进行操作。这里主要是删除和修改的功能。
  • 统计页 获取到localStorage中的记录数据,进行展示。需要解决的是对所有的记录的筛选,引入了第三方组件 datetime-picker,根据选中的年月来展示对应的记录。还引入了 echarts 柱状图,来更直观的展示数据。

6. 全局数据管理之 Vuex

考虑到多个页面都有对标签和记录数据的读取和更改操作,传参会非常繁琐且难以维护。所以采用 Vuex 对数据进行管理。这样做的好处是:

  1. 解耦:将所有数据相关的逻辑放入 store。
  2. 数据读写更方便:任何组件不管在哪里,都可以直接读写数据。
  3. 控制力更强:组件对数据的读写只能使用 store 提供的 API 进行。

我在 store 中主要实现了以下 API, 无非是对记录和标签的增删改查。供各组件进行调用。

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {},
  mutations: {
    fetchRecords() {},
    createRecord() {},
    saveRecords() {},
    fetchTags() {},
    createTag() {},
    saveTags() {},
    updateTag() {},
    removeTag() {},
  },
  actions: {},
  modules: {}
});

export default store;

7. 其他

  • 在项目中引入 Vant 根据官网提示:
# 安装 vant
npm i vant -S

按需引入组件

# 安装插件
npm i babel-plugin-import -D
// 在 babel.config.js 中配置
module.exports = {
  plugins: [
    ['import', {
      libraryName: 'vant',
      libraryDirectory: 'es',
      style: true
    }, 'vant']
  ]
};
// 接着你可以在代码中直接引入 Vant 组件
import { Button } from 'vant';

根据文档即可快速上手,对数据进行更快捷的处理和展示。更多可查看 Vant文档

  • 实现一个 ID 生成器

在对标签存储时,需要给每一个 标签添加一个独一无二的 ID,从而可以获取到对应的标签对其管理。于是我写了这么一个小工具。

let id:number = parseInt(window.localStorage.getItem('_idMax') || '0') || 0;

function createId() {
  id++;
  window.localStorage.setItem('_idMax', id.toString());
  return id;
}
export default createId;

每次从 localStorage 中读取到最大 id, 对其 +1 后返回,并且将新的 id 置为最大 id,使得所有的 id 不会发生重复。

总结

除了以上整理的思路,项目中还有许多细节不再赘述。刚开始写项目还有许多不足之处需要改进。通过这个小项目让我对 Vue 有了深刻的理解和运用。也希望通过更多的运用精进自己的技术。