项目介绍
视觉设计
使用Figma和ps显目展示
需求描述
平台:手机优先
功能:
- 记账:金额 ,标签,备注,收入还是支出
- 管理标签:添加,改名,删除
- 查看历史:
其他功能
- 页面关闭打开数据不消失(localStorage)
- 换一个手机,数据不消失(还未实现,需要使用数据库在线版)
在项目中使用的知识
- 组件:构造选项,类组件+装饰器(js or ts)
- mixin,computed,watch装饰器写法:prop,sync,v-modelM
- slot插槽,svg symbols,eslint,localStorage(5-10M)
- 表驱动编程,模块化思想,重构,命名的严谨性,
- 使用了vuex
- vue,大部分为tyscript(在学),css+html5,js;
在项目中的问题及一些知识点的分享
1. 首选安装好Node.js和vue,官网有安装步骤
2. 目录结构介绍
3. 一些小优化
- TS/JS可以通过@/目录名引入文件
- CSS/SASS可以通过~@/目录名引入文件
- 这样就不用自己计算相对路径 例如:
import Nav from '@/components/Nav.vue';
import Layout from '@/components/Layout.vue';
import Icon from '@/components/Icon.vue';
4.思路
1. 确定页面url
- #/money记账
- #/labels标签
- #/statistic统计
- 默认进入#/money
- 添加一个404页面
2.添加代码
- router/index.ts添加router,配置4个路径对应组件
- 初始化组件
- 在router传给new Vue()
- 在app组件里用给出router渲染区域
Vue.use(VueRouter);
const routes = [
{
path: '/',//默认页面
redirect: '/money'
},
{
path: '/money',
component: Money
},
{
path: '/labels',
component: Labels
},
{
path: '/statistics',
component: Statistics
},
{
path: '/labels/edit/:id',//标签编辑页面
component: EditLabel
},
{
path: '*',//404页面
component: NotFound
}
];
5. 制作NAV(底部导航栏)组件(全局在main.ts引入)
全局引入后想在哪个页面引入就引入。
Vue.component('Nav', Nav);
1.导航栏的样式:
思路: 使用flex布局,尽量不要使用fixed,因为有很多问题,flex少。
2.Layout组件&slot插槽:
layout.vue:
<template>
<div class="layout-wrapper" :class="classPrefix && `${classPrefix}-wrapper`">
<div class="content" :class="classPrefix && `${classPrefix}-content` ">
<slot/>//********
</div>
<Nav/>
</div>
</template>
money.vue:
<Layout class-prefix="layout">
....
</Layout>
定义好组件layout后就可以把money.vue中layout标签中....内容插入到layout.vue中通过 <slot/>
3.使用svg-sprite-loader引入icon
- 去iconfont.cn寻找合适的标签
- 下载svg标签(实际是一个XML文件)
- 复制到你的项目
- 在.d.ts后缀的文件中添加
declare module "*.svg" {
const content: string;
export default content;
}
- 引入svg标签在Nav.vue中
- 安装svg-sprite-loader 可以使用yarn或者npm安
- 在我没有webpack.config.js所以我需要在vue.config.js中配置(添加下面配置),加注释的地方是出现了报错,因该是Vue版本的问题,但是不影响使用
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path')
module.exports = {
publicPath:process.env.NODE_ENV === 'production'
? '/money-show/'
:'/',
lintOnSave: false,
chainWebpack: config => {
const dir = path.resolve(__dirname, 'src/assets/icons')
config.module
.rule('svg-sprite')
.test(/\.svg$/)
.include.add(dir).end()
.use('svg-sprite-loader').loader('svg-sprite-loader').options({extract:false}).end()
// .use('svg-sprite-loader-mod').loader('svg').loader('svg-sprite-loader-mod').options({extract: false}).end()
.use('svgo-loader').loader('svgo-loader')
.tap(options => ({...options, plugins: [{removeAttrs: {attrs: 'fill'}}]})).end()
// eslint-disable-next-line @typescript-eslint/no-var-requires
config.plugin('svg-sprite').use(require('svg-sprite-loader/plugin'), [{plainSprite: true}])
config.module.rule('svg').exclude.add(dir)
}
}
- 然后使用svg标签
<svg >
<use :xlink:href="name"/>
</svg>
并且在<script>中添加这样就直接引入一个文件,不用把每个标签引入一遍'../assets/icons'这是文件路径
const importAll = (requireContext: __WebpackModuleApi.RequireContext) => requireContext.keys().forEach(requireContext);
try {importAll(require.context('../assets/icons', true, /\.svg$/));} catch (error) {console.log(error);}
4.然后把标签封装为一个icno.vue文件
<template>
<svg class="icon" @click="$emit('click', $event)">
<use :xlink:href="'#'+name"/>
</svg>
</template>
<script lang="ts">
const importAll = (requireContext: __WebpackModuleApi.RequireContext) => requireContext.keys().forEach(requireContext);
try {importAll(require.context('../assets/icons', true, /\.svg$/));} catch (error) {console.log(error);}
export default {
props: ['name'],
name: 'Icon'
};
</script>
//此处的css代码来自于iconfont的help,svg标签使用方法
<style lang="scss" scoped>
.icon {
width: 1em; height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>
5.去淘宝抄一个viewport确保基本可以确保手机使用不会有问题
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
6.在svg标签中的问题(小坑)
怎么去除svg默认附带的颜色,首先下载最好不要颜色。 或者在vue.config.js中end()后添加
.use('svgo-loader').loader('svgo-loader')
.tap(options => ({...options, plugins: [{removeAttrs: {attrs: 'fill'}}]})).end()
也可以去除。需要安装一个svgo-loader的插件,同样使用yarn或者npm就可以。
7.在引入scss中出现的问题(报错,无法识别)
比如我们将一些经常要用的颜色放入一个helper.scss文件然后我们想将该文件引入就会出现问题:例如:在money.vue中引入
@import "~@/assets/style/helper.scss";
webstorm最近修复了该问题
6.money.vue组件
1.html
对money组件的布局进行实现,使用基本的html5语法,最后将每一个组件进行模块化(部分代码如下)<NumberPad /> <Tabs/> <Tags/>将这三个写成单独的vue组件引入,减少本页的代码量,可以让页面显得简单。否者处理问题难以修改
<template>
<Layout class-prefix="layout">//布局模板
<NumberPad />//输入键盘
<Tabs/>//切换按钮
<div class="notes">//备注框
<FormItem field-name="备注"
placeholder="在这里输入备注
/>
</div>
<Tags/>//标签
</Layout>
</template>
2.scss
<style lang="scss" scoped>
::v-deep .layout-content {
display: flex;
flex-direction: column-reverse;
}
.notes {
padding: 12px 0;
}
</style>
3.js(使用ts好一些)
写Vue组件的三种方式
- 用js对象
export default {data,props,methods.... }
- 用ts类
<script lang='ts'>
@Component
export default class XXX extends Vue{
xxx: string = 'hello';
@Prop(Number) xxx: number | undefined;
}
- 用js类
<script lang='js'>
@Component
export default class XXX extends Vue{
xxx = 'hello';
}
4.写好四个组件
以types组件为例:
import Vue from 'vue'
@component//装饰器
export default class Types extends Vue{
type = '- ' //'-表示支出'+'表示收入
'''这里直接写方法
}
@Component作用是告诉Tyscrtpt下面内容是一个Vue组件,里面的东西会自动被处理成data和methods。在vue中使用prop:
@Prop(type) xxx: type | undefined
5.将4个组件的Value收集起来放入recoed;
revordItem是自定义类型:
type RecordItem = {
tags: Tag[];
notes: string;
type: string;
amount: number; // 数据类型 object | string
createdAt?: string; // 类 / 构造函数
}
record: RecordItem = {
tags: [], notes: '', type: '-', amount: 0
};
最后将record保存到localStorage
saveRecords(state) {
window.localStorage.setItem('recordList',
JSON.stringify(state.recordList));
},
7.label.vue
1.html
写好基本页面,封装一个button组件,后来编辑页面也要使用,减少重复,让代码变得好看。
<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>
2.scss布局
<style lang="scss" scoped>
.tags {
background: white;
font-size: 16px;
padding-left: 16px;
> .tag {
min-height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #E6E6E6;
svg {
width: 18px;
height: 18px;
color: #666;
margin-right: 16px;
}
}
}
.createTag {
background: #767676;
color: white;
border-radius: 4px;
border: none;
height: 40px;
padding: 0 16px;
&-wrapper {
text-align: center;
padding: 16px;
margin-top: 44-16px;
}
}
</style>
3. js
后来我把所有的关于数据的操作都封装在了store目录下的三个文件中,并且将store声明为全局属性。
get tags() {
return this.$store.state.tagList;
}
beforeCreate() {
this.$store.commit('fetchTags');
}
}
在store文件下的index.ts中,使用了vuex
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','行')
}
在这个组件中遇到的一个问题:我的每个数据必须要有一个id,如果没有id我就不能准确定位到它,id不能重复。后来我想到申明一个组件将一个id一直增加存到licalstorage中,这样每次存一个数据,我就给他一个id且不会重复。
function createId() {
id++;
window.localStorage.setItem('_idMax', id.toString());
return id;
}
😝使用全局数据管理:可以直接读写。 所以之后用window来管理数据,封装好api。但是呢这样会导致全局变量太多,使得main.ts文件变得冗长,因此直接将这些api封装到store中单独管理。 (●ˇ∀ˇ●)
🤶全局状态管理(也叫全局数据管理)的好处是什么?
解耦:将所有数据相关的逻辑放入 store(也就是 MVC 中的 Model,换了个名字而已) 数据读写更方便:任何组件不管在哪里,都可以直接读写数据 控制力更强:组件对数据的读写只能使用 store 提供的 API 进行(当然也不排除有猪队友直接对 tagList 和 recordList 进行 push 等操作,这是没有办法禁止的
4.Vuex的使用
vuex学习路径。官方文档靠谱(≧∀≦)ゞ
试着在我的项目里使用vuex首先在money.vue中使用之后把labels和tags一起重构了::
Vue.use(Vuex);
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('删除失败');
}
},
fetchRecords(state) {
state.recordList = JSON.parse(window.localStorage.getItem('recordList') || '[]') as RecordItem[];
},
createRecord(state, record: RecordItem) {
const record2: RecordItem = 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) {
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;
5.在ts里使用mixin
必须让该类继承mixin,括号里填写你要导入的mixin组件,wbstorm会自动导入需要的包和文件
import {mixins} from 'vue-class-component';
import TagHelper from '@/mixins/TagHelper';
export default class Labels extends mixins(TagHelper)
6.在ts里使用computed要使用getter和setter语法
例如我要拿taglist数组的数据
get tags() {//得到vuex中的数据
return this.$store.state.tagList;
}
8.statistic组件
1.html
<Layout>
<Tabs class-prefix="type" :data-source="recordTypeList" :value.sync="type"/>
<ol v-if="groupedList.length>0">
<li v-for="(group, index) in groupedList" :key="index">
<h3 class="title">{{beautify(group.title)}} <span>¥{{group.total}}</span></h3>
<ol>
<li v-for="item in group.items" :key="item.id"
class="record">
<span>{{tagString(item.tags)}}</span>
<span class="notes">{{item.notes}}</span>
<span>¥{{item.amount}} </span>
</li>
</ol>
</li>
</ol>
<div v-else class="no-result">目前没有记录</div>
</Layout>
2.scss
<style scoped lang="scss">
.no-result{
padding: 20px;
text-align: center;
}
::v-deep {
.type-tabs-item {
background: #C4C4C4;
&.selected {
background: white;
&::after {
display: none;
}
}
}
.interval-tabs-item {
height: 48px;
}
}
%item {
padding: 8px 16px;
line-height: 24px;
display: flex;
justify-content: space-between;
align-content: center;
}
.title {
@extend %item;
}
.record {
background: white;
@extend %item;
}
.notes {
margin-right: auto;
margin-left: 16px;
color: #999;
}
</style>
3.js
采用iso8601和dayjs去处理时间显示问题。
get groupedList() {
const {recordList} = this;
const newList = clone(recordList)
.filter(r => r.type === this.type)
//排序
.sort((a, b) => dayjs(b.createdAt).valueOf() -dayjs(a.createdAt).valueOf());
if (newList.length=== 0) {return [];}
type Result = { title: string; total?: number; items: RecordItem[] }[]
const result: Result = [{title: dayjs(newList[0].createdAt).format('YYYY-MM-DD'), items: [newList[0]]}];
for (let i = 1; i < newList.length; i++) {
const current = newList[i];
const last = result[result.length - 1];
if (dayjs(last.title).isSame(dayjs(current.createdAt), 'day')) {
last.items.push(current);
} else {
result.push({title: dayjs(current.createdAt).format('YYYY-MM-DD'), items: [current]});
}
}
});
return result;
}
4.ISO时间表示
- 年由4位数字组成YYYY,或者带正负号的四或五位数字表示±YYYYY。以公历公元1年为0001年,以公元前1年为0000年,公元前2年为-0001年,其他以此类推。应用其他纪年法要换算成公历,但如果发送和接受信息的双方有共同一致同意的其他纪年法,可以自行应用。月、日用两位数字表示:MM、DD。
5.采用day.js只有2kb,表较小好用为什么使用 Day.js ?
1.2kB 下载、解析和执行更少的 JavaScript,为您的代码留出更多时间。
2.简易 Day.js 是一个轻量的处理时间和日期的 JavaScript 库,和 Moment.js 的 API 设计保持完全一样。 如果您曾经用过 Moment.js, 那么您已经知道如何使用 Day.js 。
3.不可变的 所有的 API 操作都将返回一个新的 Dayjs 对象。 这种设计能避免 bug 产生,节约调试时间。
4.国际化 Day.js 对国际化支持良好。但除非手动加载,多国语言默认是不会被打包到工程里的
9总结:
小小怪记账是一个很简单的记账网页,也是一款基于Vue,vueRount.vuex,ts的单页面应用, 这个项目是本人参考记账软件自己设计和实现的期间遇到了很多webpack和ts方面的问题,具体实现过程中有问题的都在以上进行了记录,源代码几乎全部使用了we装饰器语法。webzhuan