守财奴项目笔记

283 阅读2分钟

【前言】

守财奴是一个简易的的记账移动端网页应用, 全程使用Vue全家桶和TypeScript开发. 你可以扫描下面的二维码进行预览和使用: 项目源码

【目录】

  1. Icon 组件
  2. 页面布局
  3. 记账页面
  4. 标签页面
  5. 统计页面
  6. 状态管理与数据处理
  7. 其他

【正文】

一. 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" //归零
  }
}

四.标签页面

五.统计页面

六.状态管理与数据处理

七.其他

  1. 全局引入 scss: @import "~@/assets/style/global.scss"
  2. v::deep

可以在父组件上改写子组件的样式, 在 style 设置为 scoped 时使用

<template>
 <Tab
  class="x"
  >
</template>
<style lang="scss" scoped>
  .x ::v-deep .tab-header {
    font-size: 1.5rem;
  }
</style>