使用云和XATA构建发票生成器
#javascript #vue #xata #cloudinary

发票是由服务提供商倾向于服务收件人的文档。在易于进行交易的情况下,制定有关提供的服务,价格和所有所需细节的详细发票,正迅速成为企业主的重要技能。

本文将讨论使用Cloudinary和Xata在VUE中创建发票生成器。

Cloudinary是一个基于云的视频和图像管理平台,提供用于管理和转换网络上使用的上传资产的服务。

XATA是一个Serverless Data platform,它通过提供SaaS电子表格应用程序的可用性来简化开发人员如何与数据合作。

github

查看完整的源代码here

Netlify

我们可以访问实时演示here

先决条件

了解本文需要以下内容:

  • node.js的安装
  • JavaScript的基本知识
  • 一个云帐户(注册here
  • 使用Xata创建一个免费帐户

创建VUE应用程序

使用vue create <project-name>命令,我们将创建一个新的vue应用。
脚手架的过程将提供一个选项列表,看起来像这样:

Vue application scaffolding options

我们可以通过运行以下命令来启动VUE应用程序:

cd <project name>
npm run serve

vue将依次启动一个热加载的开发环境,默认情况下可以在http://localhost:8080访问。

造型

该项目中要使用的CSS框架是Tachyons CSS。通过在终端中运行下面的命令来安装它。

npm i tachyons

之后,通过在我们的src/main.js中添加下面的行,使其在项目中可用:

import 'tachyons/css/tachyons.css';

创建XATA数据库

要在XATA上创建数据库,我们必须登录我们的帐户或创建新的account。接下来,我们将转到用户仪表板的数据库选项卡以创建适当的数据库。

Database creation on Xata

我们将在数据库中创建一个新表格,并添加我们希望该表拥有的适当记录/字段。在这种情况下,我们创建了一个带有三个字段的user表:emailpasswordusername
这些字段表示我们将在应用程序中跟踪并保存到数据库的数据。

Table creation on Xata

在我们的VUE应用程序中设置XATA

安装XATA CLI
我们首先需要在计算机上安装XATA CLI。为此,我们将作为管理员打开命令行终端并在下面运行命令。

npm install -g @xata.io/cli@latest

在我们的项目中安装XATA

要在我们的项目中使用XATA,我们将从命令行中安装XATA软件开发套件(SDK)。

npx xata

之后,我们可以通过运行以下命令来初始化XATA以供在我们的应用程序中使用:

xata init

XATA将向我们提供我们可以选择的各种选项。最后,XATA将生成一些用于使用的文件,其中我们将拥有.xatrc.env文件。
我们需要在我们的VUE应用程序中将XATA API密钥的名称编辑为以下一个,以允许VUE应用程序拾取环境变量。

VUE_APP_XATA_API_KEY="add your api key"

云设置

对于我们应用程序的资产管理,如果您还没有,则需要通过单击here创建一个用Cloudinary创建一个帐户。创建帐户是完全免费的。
在将其配置为VUE应用程序之前,我们将需要使用以下命令安装Cloudinary Vue.js SDK

npm install cloudinary-vue

我们需要通过导航到main.js并将以下内容添加到文件中。

import Cloudinary from "cloudinary-vue";

Vue.use(Cloudinary, {
  configuration: { 
    cloudName: "XXX," //add the cloudinary cloudname here,
    secure: true }
});

我们将上传到我们要在应用程序中使用的发票模板。在这种情况下,我们使用了Canva的此发票template

创建发票仪表板

发票仪表板是大部分作品发生的地方,因为这是我们将填写将添加到发票并生成发票的数据的地方。我们将首先创建一个名为components/TheDashboardBody.vue的文件,并添加一个表单,该表单允许用户添加他们将在发票上显示的输入。

创建基本的输入数据文件

我们的仪表板页面将具有一个表格,用户可以在发票上添加他们要显示的信息。虽然我们可以手动添加所有输入字段,但我们还希望使用其他方法最小化输入字段的数量,我们将不得不添加到文件中。
为此,我们将创建一个名为util/basic-form-data.json的JSON文件,并为我们想要的基本输入字段添加所有数据,例如:

[
  {
    "id": 1,
    "inputdata": "",
    "formdata": "Invoice From",
    "type": "text",
    "otherdata": "invoicefrom"
  },
  {
    "id": 2,
    "inputdata": "",
    "formdata": "Invoice To",
    "type": "text",
    "otherdata": "invoiceto"
  },
]

创建项目输入数据文件

我们正在建造的发票还将迎合我们提供的项目或服务的部分。结果,我们将添加一个名为items-form-data.json的JSON文件,并添加我们想要的内容。喜欢:

[
  {
    "id": 11,
    "inputdata": "",
    "formdata": "Item One",
    "type": "text",
    "otherdata": "itemone"
  },
  {
    "id": 12,
    "inputdata": "",
    "formdata": "Item Two",
    "type": "text",
    "otherdata": "itemtwo"
  },
]

创建仪表板输入组件

我们将创建一个来自我们创建的JSON文件的Props的输入组件,这将帮助我们创建一个输入字段,以稍后循环。为此,我们将创建一个components/TheInputTemplate.vue文件,并添加以下代码:

<template>
  <div>
    <label for="otherData" class="db mb3 black-70 ttu fw7">{{formData}}</label>
    <input
      :value="inputData"
      :type="type"
      id="otherData"
      name="otherData"
      class="db mb3 w-90 br2 pa2 ba bw1 b--black bg-black-05"
      :placeholder="formData"
      @input="$emit('input', $event.target.value)"
    >
  </div>
</template>

<script>
export default {
  props: ["inputData", "formData", "type", "otherData"]
}
</script>

如果我们将代码与JSON文件中的项目匹配,则可以看到每个JSON Data的用例。

创建仪表板的身体

现在,我们将其组合在一起以创建一个名为components/TheDashboardBody.vue的文件并添加下面的代码。

<template>
  <div class="flex absolute vh-auto top-4 left-2 right-2 w-90 center mw9">
    <div class="mr3 bg-white w-70 br3 pa3">
      <form @submit.prevent="generateInvoice" class="flex justify-between flex-wrap">
        <div v-for="basicinfo in basicData" :key="basicinfo.id" class="w-50-l w-100">
          <TheDashboardInput v-model="basicinfo.inputdata" :formData="basicinfo.formdata" :type="basicinfo.type" :otherData="basicinfo.otherdata"/>
        </div>

        <h3 class="dark-blue w-100 f3 underline">ITEMS</h3>
        <div v-for="item in itemsData" :key="item.id" class="w-50-l w-100">
          <TheDashboardInput v-model="item.inputdata" :formData="item.formdata" :type="item.type" :otherData="item.otherdata"/>
        </div>

        <div class="w-100">
          <button type="submit" class="f6 ttu tracked black-80 bg-black pa3 br3 white bb link b--black hover-white hover-bg-dark-blue bg-animate pointer">
            Create Invoice
          </button>
        </div>
      </form>
    </div>
    <div class="bg-white w-30 br3 pa3">
      <iframe :src="iframeSRC ? iframeSRC : initialSRC" width="100%" height="100%" />
    </div>
  </div>
</template>

<script>
import TheDashboardInput from "@/components/TheDashboardInput.vue";
import itemsData from "../../util/items-form-data.json";
import basicData from "../../util/basic-form-data.json"
export default {
  components: {
  TheDashboardInput
  },
  data: ()=>({
  basicData,
  itemsData,
  basicDataInfo: {},
  itemInfo: {},
  iframeSRC: '',
  initialSRC: 'https://res.cloudinary.com/moerayo/image/upload/v1668849124/invoiced.pdf',
  }),
  methods: {
    generateBasicDataInfo() {
      let newBasicData = basicData.map(({otherdata,inputdata })=> ({[otherdata]:inputdata}))
      this.basicDataInfo = Object.assign({}, ...newBasicData)
    },
    generateItemInfo(){
      let newItemData = itemsData.map(({otherdata,inputdata })=> ({[otherdata]:inputdata}))
      this.itemInfo = Object.assign({}, ...newItemData)
    }
  }
}
</script>

从上面的代码块中,我们实现了以下内容:

  • 我们导入了我们创建的两个JSON文件-itemsDatabasicData-,并将两个添加到VUE脚本标签的data选项中,以便在我们的代码
  • 中使用。
  • 通过VUE v-for指令在数据选项中实例化的每个JSON数据循环
  • 导入了我们在合适的数据中创建并传递的TheDashboardInput组件,以填充道具选项
  • 添加了一个显示当前发票模板的iframe,尚未生成填充的发票模板
  • 创建了一个generateBasicDataInfo()generateItemInfo()方法,该方法都从JSON文件中获取各自的数据。使用JavaScript map()函数对这些数据进行重新格式化,以创建一个用otherdata作为数组keyinputdata作为value的新数组。然后使用Javascipt Object.assign()方法将此格式化数组的结果复制到对象中。这样做对于挑选通过v-model的数据至关重要

创建发票模板

生成我们的发票模板 - 即,我们将下载并发送给客户端的内容 - 我们将使用Cloudinary的功能强大的图像和文本转换功能。这些功能将使我们能够用文本叠加发票模板。为此,我们将创建一个名为components/TheInvoiceTemplate.vue的文件,并在下面添加代码:

<template>
  <div>
    <cld-image publicId="invoiced.pdf" ref="ref" >
      <cld-transformation flags="rasterize" />
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 60, text: basicDataInfo.invoicefrom,  fontWeight: 'bold'}" color="#023982" gravity="west" x="130" y="-670"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 45, text: basicDataInfo.invoiceto}" color="#023982" gravity="west" x="130" y="-480"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 20, text: basicDataInfo.date}" color="#000" gravity="west" x="560" y="-525"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 20, text: basicDataInfo.invoicenumber}" color="#000" gravity="west" x="610" y="-465"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 43, text: basicDataInfo.total}" color="#023982" gravity="west" x="990" y="-475"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 20, text: basicDataInfo.bankname}" color="#5b4f47" gravity="west" x="265" y="255"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 20, text: basicDataInfo.accountnumber}" color="#5b4f47" gravity="west" x="265" y="285"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 30, text: basicDataInfo.subtotal}" color="#000" gravity="west" x="1000" y="180"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 30, text: basicDataInfo.tax}" color="#000" gravity="west" x="1000" y="230"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 43, text: basicDataInfo.total}" color="#000" gravity="west" x="1000" y="300"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 35, text: itemInfo.itemone}" color="#000" gravity="west" x="155" y="-248"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 35, text: itemInfo.itemtwo}" color="#000" gravity="west" x="155" y="-180"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 35, text: itemInfo.itemthree}" color="#000" gravity="west" x="155" y="-80"/>
      <cld-transformation :overlay="{fontFamily: 'Poppins', fontSize: 35, text: itemInfo.itemfour}" color="#000" gravity="west" x="155" y="-1"/>
    </cld-image>  
  </div>
</template>
<script>
export default {
  name: "TheResumeTemplate",
  props: {
    basicDataInfo: {
      type: Object
    },
    itemInfo: {
      type: Object
    }
  }
}
</script>

从上面,我们实现了以下内容:

  • Cloudinary为我们提供了几个模板,可在我们的VUE应用中使用。其中之一是<cld-image /><cld-transformation />组件,我们将用来生成发票
  • 通过我们将要使用的云文件的publicId传递,该文件命名为invoiced.pdf
  • 配置了云组件以接受basicDataInfoitemInfo Props,我们将从TheDashboardbody组件传递。
  • 云组件支持资产转换,我们通过添加字体家族,颜色,字体尺寸以及我们希望将文本放置在发票上的特定位置等文本转换来利用。

要渲染此模板,我们需要在TheDashboadBody组件中导入它:


<template>
<div class="dn" v-if="showTemplate">
    <TheInvoiceTemplate :basicDataInfo="basicDataInfo" :itemInfo="itemInfo" ref="ref" />
  </div>
</template>

<script>
import TheInvoiceTemplate from "@/components/TheInvoiceTemplate.vue";
export default {
  components: {
  TheInvoiceTemplate,
  },
  data: ()=>({
  showTemplate: false,
  })
}
</script>

我们可以在此github gist中找到TheDashboardBody.vue的完整代码。
此时,TheDashboardBody.vue文件的UI应该看起来像以下内容:

The dashboard body interface

创建注册页面

我们将需要一个用户注册我们的应用程序以创建发票。为此,我们将创建一个名为views/SignupView.vue的文件,并在其中添加以下代码:


<template>
  <div class="bg-lightest-blue vh-100">
    <div class="pv5 ph2">
      <form class="ba b--dark-blue bw3 bg-white br2 mw6 w-40-m w-70 w-20-l center pa3 shadow-5" @submit.prevent="signUp">
        <h2 class="ttc tc">
          Sign up
        </h2>
        <label for="name" class="db mb2 black-70">Name</label>
        <input
          id="name"
          v-model="username"
          name="name"
          type="text"
          class="db mb3 w-100 br2 ph2 pv3 ba bw1 b--lightest-blue"
          placeholder="John Doe"
        >
        <label for="email" class="db mb2 black-70">Email</label>
        <input
          id="email"
          v-model="email"
          name="email"
          type="email"
          class="db mb3 w-100 br2 ph2 pv3 ba bw1 b--lightest-blue"
          placeholder="example@email.com"
        >
        <label for="password" class="db mb2 black-70">Password</label>
        <input
          id="password"
          v-model="password"
          name="password"
          type="password"
          class="db mb3 w-100 br2 ph2 pv3 ba bw1 b--lightest-blue"
          placeholder="••••••••"
        >
        <button type="submit" class="center db pa3 mb3 tracked bg-dark-blue ba br3 white pointer hover-black hover-bg-lightest-blue bg-animate pointer">
          Sign up
        </button>
        <p>Already have an account? <a href="/signin" class="black-70 b">Sign in</a> </p>
      </form>
    </div>
  </div>
</template>

这样,我们已成功为我们的注册页面创建了一个接口。现在,我们将添加身份验证和授权的功能。

我们将使用XATA实现我们的身份验证目标。为此,我们将其添加到SignupView.view文件:

<script>
import { getXataClient } from '@/xata'
export default {
  name: 'signup',
  data: () => ({
    username: '',
    email: '',
    password: '',
  }),
  methods: {
    async signUp() {
      const xata = getXataClient()
      const user = await xata.db.users.filter('username', this.username).getFirst()
      if (!user) {
        await xata.db.users.create({
          username: this.username,
          password: this.password,
          email: this.email
        }).then((res) => {
          this.$router.push({path:`/dashboard/${res.username}`, params: user})
        })
        this.$notify({type: 'success', text: "Account creation successful!" })
      }
    }
  }
}
</script>

从上面的代码块中,我们实现了以下内容:

  • 导入XATA客户端
  • 检查用户是否存在并创建新用户,如果当前用户不存在
  • 在成功创建帐户时,用户的视图将更改为仪表板,从他们可以生成发票的仪表板。

此时,我们的应用程序应该看起来像以下内容:

signup interface

创建标志页

我们还需要创建选项,以允许已经创建帐户的人签名。为此,我们将创建一个名为SigninView.vue的文件,并添加以下代码:


<template>
  <div class="bg-lightest-blue vh-100">
    <div class="pv5 ph2">
      <form class="ba b--dark-blue bw3 bg-white br2 mw6 w-40-m w-70 w-20-l center pa3 shadow-5" @submit.prevent="signIn">
        <h2 class="ttc tc">
          Sign In
        </h2>
        <label for="email" class="db mb2 black-70">Email</label>
        <input
          id="email"
          v-model="email"
          name="email"
          type="email"
          class="db mb3 w-100 br2 ph2 pv3 ba bw1  b--lightest-blue"
          placeholder="example@email.com"
        >
        <label for="password" class="db mb2 black-70">Password</label>
        <input
          id="password"
          v-model="password"
          name="password"
          type="password"
          class="db mb3 w-100 br2 ph2 pv3 ba bw1  b--lightest-blue"
          placeholder="••••••••"
        >
        <button type="submit" class="center db pa3 mb3 tracked bg-dark-blue ba br3 white pointer hover-black hover-bg-lightest-blue bg-animate pointer">
          Sign in
        </button>
      </form>
    </div>
  </div>
</template>

以上为我们创建了一个登录接口。但是,我们需要围绕它包裹一些功能,以使其按预期工作。
为此,我们将在SigninView.vue中添加下面的代码。


<script>
import { getXataClient } from '@/xata'
export default {
  data: () => ({
    email: '',
    password: ''
  }),
  methods: {
    async signIn() {
      const xata = getXataClient()
      const user = await xata.db.users.filter('email', this.email).getFirst()
      if (!this.email || !this.password ){
        this.$notify({type: 'error', text: "Please fill all empty fields"})
      } else if (this.email !== user.email || this.password !== user.password){
        this.$notify({type: 'error', text: "Incorrect credentials"})
        this.email = '';
        this.password = '';
      } else {
        this.$router.push({path:`/dashboard/${user.username}`, params: user})
        this.$notify({type: 'success', text: "Login successful!"})
      }
    }
  }
}
</script>

从上面,我们创建了一个称为signIn函数的函数,可执行以下操作:

  • 导入XATA客户端
  • 检查用户是否通过emailpassword,然后返回适当的错误,如果不可用
  • 交叉检查输入的电子邮件和密码以确保与数据库中的电子邮件和密码匹配
  • 如果支票通过,则该页面被路由到仪表板,用户可以在其中生成n发票

此时,我们的界面看起来像这样:

Signin interface

此时,我们的完整应用程序看起来如下:

Complete application interface

结论

本文讨论使用Cloudinary和XATA生成发票。 Cloudinary用于资产管理和文本转换,而我们使用XATA进行身份验证。

资源