绿豆记账项目笔记

301 阅读4分钟

项目名

绿豆记账

开发背景

我在学习Vue的过程中,为了更好的理解与应用,开发出的一个记账工具。

需求分析

随着互联网行业的快速发展,人们开始使用手机支付,而这种生活方式的改变,大家逐渐觉得,手机里的钱,不知道怎么就没了,记账可以帮助我们合理消费,而纸上记账很不方便,也容易丢,记性不好的回家记账时,已经忘记了今日花销在哪里,而且也达不到控制消费的目的。手机记账工具就和方便,吃完饭玩手机时顺便就记一笔,所以我选择开发了这款绿豆记账应用。

项目介绍

绿豆记账是一款极简单页面记账应用。简单分为四个页面:记账页,标签页,编辑页,和统计页面。记账页面有标签选择,记账备注,记账金额,和保存。标签页提供新增标签功能,并且和记账页面的标签同步更新。编辑页面,可以删除标签。统计页面,完成近期记账的统计,和每日收入支出金额合计。

模块概览

详细设计

页面布局

使用Vue-router进行路由管理。实现访问四个页面的功能,当用户进入根目录时会默认定位到/money/路径并加载Money组件。

将Nva组件做成全局组件

实现四个页面的互相切换功能。

注意:手机页面开发不要使用Fixed布局,使用Flex布局来进行开发.

<template>
  <nav>
    <router-link to="/labels" class="item" active-class="selected">
      <Icon name="label" />标签
    </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>
</template>

Layout组件与slot插槽

新建Layout.vue文件夹,用Layout组件把重复的代码抽离出来,分装在Layout组件中,使代码更加简洁,清楚. 不重复的代码用slot插槽,插进来

**注意:**手机页面开发不要使用Fixed布局,使用Flex布局来进行开发.

<template>
  <nav>
    <router-link to="/labels" class="item" active-class="selected">
      <Icon name="label" />标签
    </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>
</template>

Layout组件与slot插槽

新建Layout.vue文件夹,用Layout组件把重复的代码抽离出来,分装在Layout组件中,使代码更加简洁,清楚. 不重复的代码用slot插槽,插进来

整体布局设计

底部导航栏

引入SVG

使用svg-sprite-loader 引入icon

第一步: 添加配置代码 直接引入svg TS会报错,在shims-vue.d.ts中,添加如下代码,消除报错。

declare module "*.svg" {
  const content: string;
  export default content;
}

第二步:安装loader

打开终端 安装 svg-sprite-loader

代码如下

yarn add svg-sprite-loader -D

第三步:配置vue.confing.js

在vue.confing.js完成配置详细代码

/* eslint-disable */
const path = require("path");

module.exports = {
  publicPath: process.env.NODE_ENV === "production" ? "/Ledger-website/" : "/",
  lintOnSave: false,
  chainWebpack: (config) => {
    const dir = path.resolve(__dirname, "src/assets/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 目录
  },
};

记账页面

Money组件及布局如下

Tag

Tags组件功能 新建标签,标签选择高亮。 要使用TS 必须引入class类,必须加@Component

<script lang="ts">
import Vue from "vue";
import { Component, Prop } from "vue-property-decorator";
import { mixins } from "vue-class-component";
import TagHelper from "../../mixins/TagHelper";
@Component
export default class Tags extends mixins(TagHelper) {
  selectedTags: string[] = [];
  get tagList() {
    return this.$store.state.tagList;
  }
  created() {
    this.$store.commit("fetchTags");
  }

  toggle(tag: string) {
    const index = this.selectedTags.indexOf(tag);
    if (index >= 0) {
      this.selectedTags.splice(index, 1);
    } else {
      this.selectedTags.push(tag);
    }
    this.$emit("update:value", this.selectedTags);
  }
}
</script>

scoped的作用

当vue发现使用scoped后,会自动给div添加字符串,来区分别的div,达成只能在范围内使用的目的

<style lang="scss" scoped>

TS组件@prop装饰器

Vue 单文件组件的三种写法

1.用 JS 对象

export default {data,props,methods,created,...}

2.用 TS 类
  export default class XXX extends Vue{
  xxx: string ="hi";
  @prop(Number) xxx:number|undefined;
  }

3.用 JS 类
@Component
export default class XXX extends Vue{
xxx="hi"
}

Notes模块 v-model

下面代码可以用 v-model="x"代替

:value="x" 
@input="x"=$event.target.value"

标签页+标签编辑页

标签页功能

实现新增标签功能,并且和记账页面所显示的标签同步,实时更新,初始状态下,自动添加 衣,食,住,行四个显示标签

标签编辑页功能

删除标签
<template>
<Layout>
  <div class="tags">
    <router-link class="tag" v-for="tag in tags" :key="tag.id" :to="`/labels/edit/${tag.id}`">
      <span>{{tag.name}}</span>
      <Icon name="right" />
    </router-link>
  </div>
  <div class="createTag-wrapper">
    <Button class="createTag" @click="createTag">新建标签</Button>
  </div>
</Layout>
</template>

@Component({
components: { Button, Notes },
})
export default class EditLabel extends Vue {
get currentTag() {
  return this.$store.state.currentTag;
}
created() {
  const id = this.$route.params.id;
  this.$store.commit("fetchTags");
  this.$store.commit("setCurrentTag", id);
  if (!this.currentTag) {
    this.$router.replace("/404");
  }
}
update(name: string) {
  if (this.currentTag) {
    this.$store.commit("updateTag", {
      id: this.currentTag.id,
      name,
    });
  }
}

remove() {
  if (this.currentTag) {
    this.$store.commit("removeTag", this.currentTag.id);
  }
}
goBack() {
  this.$router.back();
}
}

  if (!this.tag) {

</script>

统计页面

功能

完成今日支出或收入的金额的总计,显示近期所记的收入和支出,

::v-deep

功能

可以在父组件上更改子组件的样式

::v-deep {
.type-tabs-item {
  background: rgb(254, 219, 224);
  &.selected {
    background: rgb(254, 219, 224);
    &::after {
      display: none;
    }
  }
}
.interval-tabs-item {
  height: 48px;
}
}

Vuex初体验

Vuex action用于异步操作,我们使用了stare 将代码改写为Vuex,部分代码展示

import Vue from "vue";
import Vuex from "vuex";
import clone from "@/lib/clone";
import createId from "@/lib/createId";
import router from "@/router";
Vue.use(Vuex);
// const localStorageKeyName = "recordList";
const localStorageKeyName = "tagList";

const store = new Vuex.Store({
state: {
  recordList: [],
  createRecordError: null,
  createTagError: null,
  tagList: [],
  currentTag: undefined,
} as RootState,

mutations: {
  setCurrentTag(state, id: string) {
    state.currentTag = state.tagList.filter((t) => t.id === id)[0];
  },
  updateTag(state, payload: { id: string; name: string }) {
    const { id, name } = payload;
    const idList = state.tagList.map((item) => item.id);
    if (idList.indexOf(id) >= 0) {
      const names = state.tagList.map((item) => item.name);
      if (names.indexOf(name) >= 0) {
        window.alert("标签名重复了");
      } else {
        const tag = state.tagList.filter((item) => item.id === id)[0];
        tag.name = name;
        store.commit("saveTags");
      }
    }
  },
  removeTag(state, id: string) {
    let index = -1;
    for (let i = 0; i < state.tagList.length; i++) {
      if (state.tagList[i].id === id) {
        index = i;
        break;
      }
    }
    if (index >= 0) {
      state.tagList.splice(index, 1);
      store.commit("saveTags");
      router.back();
    } else {
      window.alert("删除失败");
    }
  },
  // saveTags() {
  //   window.localStorage.setItem(
  //     localStorageKeyName,
  //     JSON.stringify(this.tagList)
  //   );
  // },

  fetchRecords(state) {
    state.recordList = JSON.parse(
      window.localStorage.getItem("recordList") || "[]"
    ) as RecordItem[];
  },
  createRecord(state, record: RecordItem) {
    const record2 = clone(record);
    record2.createdAt = new Date().toISOString();
    state.recordList.push(record2);
    store.commit("saveRecords");
  },
  saveRecords(state) {
    window.localStorage.setItem(
      "recordList",
      JSON.stringify(state.recordList)
    );
  },
  fetchTags(state) {
    state.tagList = JSON.parse(
      window.localStorage.getItem("tagList") || "[]"
    );

    if (!state.tagList || state.tagList.length === 0) {
      store.commit("createTag", "衣");
      store.commit("createTag", "食");
      store.commit("createTag", "住");
      store.commit("createTag", "行");
    }![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a18a9f9ba2dc4c6da3abba3b2860d9f6~tplv-k3u1fbpfcp-zoom-1.image)
  },
  createTag(state, name: string) {
    state.createTagError = null;
    const names = state.tagList.map((item) => item.name);
    if (names.indexOf(name) >= 0) {
      state.createTagError = new Error("tag name duplicated");
      return;
    }
    const id = createId().toString();
    state.tagList.push({ id, name: name });
    store.commit("saveTags");
  },
  saveTags(state) {
    window.localStorage.setItem("tagList", JSON.stringify(state.tagList));
  },
},
});

export default store;

绿豆记账项目源码链接

源码已部署到github上,点击链接查看源码

项目预览

可以通过预览链接预览和使用绿豆记账

也可以扫下方二维码来预览和使用