【前言】
守财奴是一个简易的的记账移动端网页应用, 全程使用Vue全家桶和TypeScript开发. 你可以扫描下面的二维码进行预览和使用: 项目源码
【目录】
- Icon 组件
- 页面布局
- 记账页面
- 标签页面
- 统计页面
- 状态管理与数据处理
- 其他
【正文】
一. Icon 组件
本项目使用vue-cli 作为脚手架开发.包含了我所需要的配置: 使用 Vue + Vuex + Vue-router 作为视图层的开发.使用 TypeScript 代替 JavaScript.使用 SCSS 代替 CSS. 但是还有一些配置需要自己完成.
SVG loader 的配置
由于使用了 TypeScript, 我们需要在声明文件(shims-vue.d.ts)里去声明一下 svg:
declare module "*.svg" {
const content: string;
export default content;
}
这样在引入 svg 时不会报错了.
接下来引入 svg 需要的 loaders: yarn add svg-sprite-loader svgo-loader. 然后在vue.config.js里配置:
const path = require("path");
module.exports = {
chainWebpack: config => {
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
.loader("svg-sprite-loader")
.options({ extract: false })
.end()
.use("svgo-loader")
.loader("svgo-loader")
.tap(options => ({
...options,
plugins: [{ removeAttrs: { attrs: "fill" } }] // 删除svg的fill属性
}))
.end();
config
.plugin("svg-sprite")
.use(require("svg-sprite-loader/plugin"), [{ plainSprite: true }]);
config.module.rule("svg").exclude.add(dir); // 其他 svg loader 不会解析 icons 目录
}
};
svg-sprite-loader的作用是解析 svg 文件, 但要注意, 如果使用 WebStorm 时, 且svg-sprite-loader的版本低于4.2.0时, 引入全局 SCSS 文件会报错(是 WebStorm code assistance analyzer 的错, 它没能解析出来). 不过在4.2.0及以上的版本中这个 bug 已被修复
svgo-loader帮助我们删除 svg 里的fill属性, 这样使得我可以自定义 svg 的颜色, 之前一旦被定好了颜色就修改不了.
写一个 Icon 组件
在开始写代码前, 先不要管具体的实现细节. 而是想我在使用这个 Icon 组件时的写法:
<Icon name="money" />
上面的形式是最简单也是最简洁的.
开始实现 Icon 组件的模板很容易写:
<template>
<svg class="icon">
<use :xlink:href="`#${name}`"></use>
</svg>
</template>
<script lang="ts">
export default {
name: "Icon",
props: ["name"]
};
</script>
<style scoped>
/* iconfont推荐的默认样式 */
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>
最主要的问题是如何一次性导入所有的 icon 到组件里以方便复用, 下面是解决代码:
const importAll = (requireContext: __WebpackModuleApi.RequireContext) =>
requireContext.keys().forEach(requireContext);
try {
importAll(require.context("../assets/icons", true, /\.svg$/));
} catch (e) {
console.log(e);
}
二.页面布局
守财奴有三个主页面: 记账, 标签, 统计三个, 还有一个 404 页面. 我使用 Vue-router 进行路由管理. 当用户进入根目录时会重定向到/money/路径并加载Money组件.
Nav 组件
为了在三个主页面之间切换, 我们创建了一个Nav组件.
<template>
<nav>
<router-link to="/money" class="item" active-class="selected">
<Icon name="money" />
记账
</router-link>
<router-link to="/labels" class="item" active-class="selected">
<Icon name="label" />
标签
</router-link>
<router-link to="/statistics" class="item" active-class="selected">
<Icon name="chart" />
统计
</router-link>
</nav>
</template>
<script lang="ts">
export default {
name: "Nav"
};
</script>
<style lang="scss" scoped>
@import "~@/assets/style/global.scss";
nav {
@extend %outerShadow;
display: flex;
flex-direction: row;
font-size: 12px;
> .item {
padding: 2px 0;
width: 33.33333%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
.icon {
width: 32px;
height: 32px;
}
}
> .item.selected {
color: #409eff;
}
}
</style>
在 SCSS 里, 我们可以把一些 CSS 代码抽离出来, 然后再在另外的地方复用. 可以使用@extend 语法, 如高亮部分所展示.
Layout组件
Layout组件占领了当前全部视图. 如下图所示: 我们使用slot占位主页面的组件, 然后在页面底部放上Nav组件.结构如下:
Copyright © wanmao
Link: https://wanmaoor.github.io/tech-blog/account/2.html
<template>
<div class="container">
<div class="view">
<slot />
</div>
<Nav />
</div>
</template>
<script lang="ts">
export default {
name: "Layout"
};
</script>
<style scoped>
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.view {
flex-grow: 1;
overflow: auto;
}
</style>
flex-grow: 1让当前 flex item 占满剩下的空间. 也就是除了 nav 之外的剩余高度(默认占满宽度).
三、记账页面
Money 组件及布局如下:
.sync修饰符实现响应式
通过.sync修饰符绑定则可以统一在父组件上收集各个子组件的状态.
Copyright © wanmao
Link: https://wanmaoor.github.io/tech-blog/account/3.html
<Panel :expression.sync="record.amount" />
<Tab :value.sync="record.type" />
<InputItem :notes.sync="record.notes" />
<Tag :labels.sync="labels" :selectedTags.sync="record.tags" />
Tag组件解析
此组件的看点只有switchTag函数.当被触发时, 它首先根据传入的标签名判断已有的selectedTags数组里是否包含这个标签, 有则删除, 没有则添加.从而影响样式的更新.其次,发射更新事件(以配合.sync), 返回值是选中的标签组成的数组.
@Emit("update:selectedTags")
switchTag(label: string) {
const index = this.selectedTags.indexOf(label)
if (index >= 0) {
this.selectedTags.splice(index, 1)
} else {
this.selectedTags.push(label)
}
return this.selectedTags
}
InputItem组件解析
该组件比较简单, 这里只提一个知识点, 在模板里的事件函数:
<template>
<input
:placeholder="placeholder"
:value="notes"
@input="handleChange($event.target.value)"
type="text"
/>
</template>
<script>
export default {
handleChange(val) {
this.$emit("update:notes", val);
}
};
</script>
Tab组件
Panel组件
布局
键盘是使用浮动来做的, 如图所示:
除 Ok 键以外的 button 都向左浮动, OK 键向右浮动. 每个 button 的width为25%. 而 0 键宽度单独设置为50%. 最后要在浮动元素的父元素上清除浮动:
parentElement {
clear: both;
display: block;
}
获取用户输入值
通过点击事件的e.target.textContent可以获取键盘对应数值.
防御性编程
为防止用户做出一些非正常输入, 做防御性编程是很重要的.
输入数字的最大长度只能有 16 位
if (this.output.length === 16) {
return;
}
初始为 0, 当输入数字后 0 应该消失
if (this.output === "0") {
if ("0123456789".indexOf(input) >= 0) {
this.output = input;
} else {
this.output += input;
}
return;
}
当我们输入的值是"0123456789"里的任意一个, 那么原来的0都会被替换, 否则累加, 在这里只有输入小数点才会累加.
只能输一个小数点
if (this.output.indexOf(".") >= 0 && input === ".") {
return;
}
当输入里有小数点时且输入中又含有小数点则返回.
退格
deleteNumber() {
const length = this.output.length
this.output = this.output.substr(0, length - 1) // 获取除了最后一个字符以外的新字符串
if (length === 1) {
this.output = "0" //归零
}
}
四.标签页面
五.统计页面
六.状态管理与数据处理
七.其他
- 全局引入 scss: @import "~@/assets/style/global.scss"
- v::deep
可以在父组件上改写子组件的样式, 在 style 设置为 scoped 时使用
<template>
<Tab
class="x"
>
</template>
<style lang="scss" scoped>
.x ::v-deep .tab-header {
font-size: 1.5rem;
}
</style>