项目名
绿豆记账
开发背景
我在学习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;
}
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"
}
@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", "行");
}
},
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上,点击链接查看源码
项目预览
可以通过预览链接预览和使用绿豆记账
也可以扫下方二维码来预览和使用