Для веб-разработчиков создание приложения для Windows требует значительного обучения. Однако теперь есть решение для преобразования веб-приложения JavaScript в приложение для Windows с помощью Electron без особых усилий. React можно легко объединить с Electron для создания приложения для Windows.

Electron - это среда выполнения на основе браузера Chromium, которая позволяет нам запускать веб-приложения как собственные настольные приложения. Он обертывает Chromium вокруг нашего веб-приложения, так что оно выглядит как настольное приложение. Мы можем делать некоторые вызовы собственных API-интерфейсов, например, манипулировать файлами, изменять меню и взаимодействовать с некоторым оборудованием.

Создать приложение Electron Vue.js легко, если мы будем использовать надстройку vue-cli-plugin-electronic-builder для Vue CLI 3. Он взят из https://github.com/nklayman/vue-cli- плагин-электрон-строитель .

В этой статье мы создадим приложение Vue Electron, работающее в Windows. Это приложение адресной книги, которое позволяет нам добавлять контакты и сохранять их с помощью серверной части, обслуживающей файл JSON.

Чтобы начать сборку приложения, мы начнем с установки Vue CLI, запустив:

npm i -g @vue/cli

Затем мы создаем наш проект Vue.js, запустив vue create address-book-app. Обязательно выберите «Выбрать функции вручную», а затем выберите включение Babel, Vuex и Vue Router. Это создаст исходные файлы для нашего приложения. Затем мы добавляем Electron в наше приложение, выполнив:

vue add electron-builder

Эта команда добавляет необходимые файлы и скрипты, чтобы мы могли встроить наше приложение в приложение Electron, а также предварительно просмотреть наше приложение Vue.js, работающее в окне Electron, что позволяет нам выполнять отладку внутри него, а не в обычном браузере.

Как только мы добавим это, нам нужно добавить наши собственные библиотеки. Нам нужны Axios для выполнения HTTP-запросов, Bootstrap-Vue для стилизации и Vee-Validate для проверки формы. Мы устанавливаем их, запустив:

npm i axios bootstrap-vue vee-validate

в папке проекта.

Теперь, когда мы установили наши библиотеки, мы можем приступить к созданию нашего приложения адресной книги. Начнем с создания контактной формы для добавления и редактирования наших контактов. Добавляем ContactFome.vue файл в папку components и добавляем:

<template>
  <ValidationObserver ref="observer" v-slot="{ invalid }">
    <b-form @submit.prevent="onSubmit" novalidate>
      <b-form-group label="First Name">
        <ValidationProvider name="firstName" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.firstName"
            required
            placeholder="First Name"
            name="firstName"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">First name is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-form-group label="Last Name">
        <ValidationProvider name="lastName" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.lastName"
            required
            placeholder="Last Name"
            name="lastName"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Last name is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-form-group label="Address">
        <ValidationProvider name="addressLineOne" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.addressLineOne"
            required
            placeholder="Address"
            name="addressLineOne"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Address is required.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-form-group label="City">
        <ValidationProvider name="city" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.city"
            required
            placeholder="City"
            name="city"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">City is required.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-form-group label="Postal Code">
        <ValidationProvider
          name="postalCode"
          rules="required|postal_code:country"
          v-slot="{ errors }"
        >
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.postalCode"
            required
            placeholder="Postal Code"
            name="postalCode"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Postal code is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-form-group label="Country">
        <ValidationProvider name="country" rules="required" v-slot="{ errors }">
          <b-form-select
            :options="countries"
            :state="errors.length == 0"
            v-model="form.country"
            required
            placeholder="Country"
            name="country"
          ></b-form-select>
          <b-form-invalid-feedback :state="errors.length == 0">Country is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-form-group label="Email">
        <ValidationProvider name="email" rules="required|email" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.email"
            required
            placeholder="Email"
            name="email"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-form-group label="Phone">
        <ValidationProvider name="phone" rules="required|phone:country" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.phone"
            required
            placeholder="Phone"
            name="phone"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-form-group label="Age">
        <ValidationProvider
          name="age"
          rules="required|min_value:0|max_value:200"
          v-slot="{ errors }"
        >
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.age"
            required
            placeholder="Age"
            name="age"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
<b-button type="submit" variant="primary">Submit</b-button>
      <b-button type="reset" variant="danger" @click="cancel()">Cancel</b-button>
    </b-form>
  </ValidationObserver>
</template>
<script>
import { COUNTRIES } from "@/helpers/exports";
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
  name: "ContactForm",
  mixins: [requestsMixin],
  props: {
    edit: Boolean,
    contact: Object
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
if (this.edit) {
        await this.editContact(this.form);
      } else {
        await this.addContact(this.form);
      }
      const response = await this.getContacts();
      this.$store.commit("setContacts", response.data);
      this.$emit("saved");
    },
    cancel() {
      this.$emit("cancelled");
    }
  },
  data() {
    return {
      form: {},
      countries: COUNTRIES.map(c => ({ value: c.name, text: c.name }))
    };
  },
  watch: {
    contact: {
      handler(c) {
        this.form = c || {};
      },
      deep: true,
      immediate: true
    }
  }
};
</script>

В форме мы оборачиваем каждый ввод с помощью ValidationProvider, чтобы получить подтверждение формы для каждого поля вместе с ошибками проверки формы. Мы добавляем :state=”errors.length == 0" в каждый b-form-input, чтобы получить правильное сообщение проверки, отображаемое и оформленное должным образом для каждого ввода. Объект errors имеет форму сообщений об ошибках проверки для каждого ввода. Нам также необходимо указать опору name в ValidationProvider и b-form-input, чтобы правила проверки формы применялись к входным данным внутри ValidationProvider.

Мы используем ValidationObserver для отслеживания ошибок проверки в нашей форме, которая находится внутри. У нас есть свойство ref=”observer” в ValidationObserver, чтобы мы могли вызвать await this.$refs.observer.validate(); для проверки нашей формы. observer - наша ссылка на компонент ValidationObserver. Мы помещаем форму внутрь компонента ValidationObserver, чтобы мы могли проверить всю форму. С Vee-Validate мы получаем функцию this.$refs.observer.validate(), когда используем ValidationObserver, как мы это делали в приведенном выше коде. Он возвращает обещание, которое принимает значение true, если форма действительна, и false в противном случае. Поэтому, если он принимает значение false, мы не запускаем остальную часть кода функции.

В этой форме есть проверка между полями. Поле страны проверяется перед проверкой формата номера телефона и почтового индекса. Мы добавим эти правила проверки в main.js позже.

Для отображения сообщений об ошибках проверки формы у нас есть объект errors, доступный только в шаблоне. Слоты с заданной областью, встроенные в компоненты Vee-Validate, предоставляют объект errors, который имеет сообщения проверки.

В атрибуте rules каждого поля мы передаем имена правил, разделенные вертикальной чертой. Правила phone и postal_code - это правила перекрестного поля. country после двоеточия - это свойство имени поля country, то есть country.

Компоненты формы и ввода предоставляются BootstrapVue.

Когда нажимается кнопка отправки формы, мы вызываем кнопку onSubmit. Функция onSubmit передается в опору submit.prevent, чтобы предотвратить действие отправки по умолчанию, поэтому мы можем использовать Ajax для отправки формы.

В этой функции мы используем this.$refs.observer.validate(); для проверки формы. Затем после этого мы вызываем editContact или addContact в зависимости от того, верно edit prop или нет. Мы передали их из файла HomePage.vue, который мы добавим. Эти 2 функции предназначены для выполнения HTTP-запросов к нашему серверу для отправки наших данных.

После вызова любой из функций мы получаем последние данные и помещаем их в наше хранилище Vuex с помощью:

this.$store.commit("setContacts", response.data);

this.$store предоставляется Vuex.

Затем мы отправляем событие saved в HomePage.vue, чтобы закрыть модальные окна.

Страны импортируются из другого файла, а свойство contact передается из HomePage.vue, когда пользователь выбирает запись для редактирования.

Затем создайте папку helpers в папке src и добавьте файл exports.js. Там добавьте:

export const COUNTRIES = [
  { name: "Afghanistan", code: "AF" },
  { name: "Aland Islands", code: "AX" },
  { name: "Albania", code: "AL" },
  { name: "Algeria", code: "DZ" },
  { name: "American Samoa", code: "AS" },
  { name: "AndorrA", code: "AD" },
  { name: "Angola", code: "AO" },
  { name: "Anguilla", code: "AI" },
  { name: "Antarctica", code: "AQ" },
  { name: "Antigua and Barbuda", code: "AG" },
  { name: "Argentina", code: "AR" },
  { name: "Armenia", code: "AM" },
  { name: "Aruba", code: "AW" },
  { name: "Australia", code: "AU" },
  { name: "Austria", code: "AT" },
  { name: "Azerbaijan", code: "AZ" },
  { name: "Bahamas", code: "BS" },
  { name: "Bahrain", code: "BH" },
  { name: "Bangladesh", code: "BD" },
  { name: "Barbados", code: "BB" },
  { name: "Belarus", code: "BY" },
  { name: "Belgium", code: "BE" },
  { name: "Belize", code: "BZ" },
  { name: "Benin", code: "BJ" },
  { name: "Bermuda", code: "BM" },
  { name: "Bhutan", code: "BT" },
  { name: "Bolivia", code: "BO" },
  { name: "Bosnia and Herzegovina", code: "BA" },
  { name: "Botswana", code: "BW" },
  { name: "Bouvet Island", code: "BV" },
  { name: "Brazil", code: "BR" },
  { name: "British Indian Ocean Territory", code: "IO" },
  { name: "Brunei Darussalam", code: "BN" },
  { name: "Bulgaria", code: "BG" },
  { name: "Burkina Faso", code: "BF" },
  { name: "Burundi", code: "BI" },
  { name: "Cambodia", code: "KH" },
  { name: "Cameroon", code: "CM" },
  { name: "Canada", code: "CA" },
  { name: "Cape Verde", code: "CV" },
  { name: "Cayman Islands", code: "KY" },
  { name: "Central African Republic", code: "CF" },
  { name: "Chad", code: "TD" },
  { name: "Chile", code: "CL" },
  { name: "China", code: "CN" },
  { name: "Christmas Island", code: "CX" },
  { name: "Cocos (Keeling) Islands", code: "CC" },
  { name: "Colombia", code: "CO" },
  { name: "Comoros", code: "KM" },
  { name: "Congo", code: "CG" },
  { name: "Congo, The Democratic Republic of the", code: "CD" },
  { name: "Cook Islands", code: "CK" },
  { name: "Costa Rica", code: "CR" },
  {
    name: 'Cote D"Ivoire',
    code: "CI"
  },
  { name: "Croatia", code: "HR" },
  { name: "Cuba", code: "CU" },
  { name: "Cyprus", code: "CY" },
  { name: "Czech Republic", code: "CZ" },
  { name: "Denmark", code: "DK" },
  { name: "Djibouti", code: "DJ" },
  { name: "Dominica", code: "DM" },
  { name: "Dominican Republic", code: "DO" },
  { name: "Ecuador", code: "EC" },
  { name: "Egypt", code: "EG" },
  { name: "El Salvador", code: "SV" },
  { name: "Equatorial Guinea", code: "GQ" },
  { name: "Eritrea", code: "ER" },
  { name: "Estonia", code: "EE" },
  { name: "Ethiopia", code: "ET" },
  { name: "Falkland Islands (Malvinas)", code: "FK" },
  { name: "Faroe Islands", code: "FO" },
  { name: "Fiji", code: "FJ" },
  { name: "Finland", code: "FI" },
  { name: "France", code: "FR" },
  { name: "French Guiana", code: "GF" },
  { name: "French Polynesia", code: "PF" },
  { name: "French Southern Territories", code: "TF" },
  { name: "Gabon", code: "GA" },
  { name: "Gambia", code: "GM" },
  { name: "Georgia", code: "GE" },
  { name: "Germany", code: "DE" },
  { name: "Ghana", code: "GH" },
  { name: "Gibraltar", code: "GI" },
  { name: "Greece", code: "GR" },
  { name: "Greenland", code: "GL" },
  { name: "Grenada", code: "GD" },
  { name: "Guadeloupe", code: "GP" },
  { name: "Guam", code: "GU" },
  { name: "Guatemala", code: "GT" },
  { name: "Guernsey", code: "GG" },
  { name: "Guinea", code: "GN" },
  { name: "Guinea-Bissau", code: "GW" },
  { name: "Guyana", code: "GY" },
  { name: "Haiti", code: "HT" },
  { name: "Heard Island and Mcdonald Islands", code: "HM" },
  { name: "Holy See (Vatican City State)", code: "VA" },
  { name: "Honduras", code: "HN" },
  { name: "Hong Kong", code: "HK" },
  { name: "Hungary", code: "HU" },
  { name: "Iceland", code: "IS" },
  { name: "India", code: "IN" },
  { name: "Indonesia", code: "ID" },
  { name: "Iran, Islamic Republic Of", code: "IR" },
  { name: "Iraq", code: "IQ" },
  { name: "Ireland", code: "IE" },
  { name: "Isle of Man", code: "IM" },
  { name: "Israel", code: "IL" },
  { name: "Italy", code: "IT" },
  { name: "Jamaica", code: "JM" },
  { name: "Japan", code: "JP" },
  { name: "Jersey", code: "JE" },
  { name: "Jordan", code: "JO" },
  { name: "Kazakhstan", code: "KZ" },
  { name: "Kenya", code: "KE" },
  { name: "Kiribati", code: "KI" },
  {
    name: 'Korea, Democratic People"S Republic of',
    code: "KP"
  },
  { name: "Korea, Republic of", code: "KR" },
  { name: "Kuwait", code: "KW" },
  { name: "Kyrgyzstan", code: "KG" },
  {
    name: 'Lao People"S Democratic Republic',
    code: "LA"
  },
  { name: "Latvia", code: "LV" },
  { name: "Lebanon", code: "LB" },
  { name: "Lesotho", code: "LS" },
  { name: "Liberia", code: "LR" },
  { name: "Libyan Arab Jamahiriya", code: "LY" },
  { name: "Liechtenstein", code: "LI" },
  { name: "Lithuania", code: "LT" },
  { name: "Luxembourg", code: "LU" },
  { name: "Macao", code: "MO" },
  { name: "Macedonia, The Former Yugoslav Republic of", code: "MK" },
  { name: "Madagascar", code: "MG" },
  { name: "Malawi", code: "MW" },
  { name: "Malaysia", code: "MY" },
  { name: "Maldives", code: "MV" },
  { name: "Mali", code: "ML" },
  { name: "Malta", code: "MT" },
  { name: "Marshall Islands", code: "MH" },
  { name: "Martinique", code: "MQ" },
  { name: "Mauritania", code: "MR" },
  { name: "Mauritius", code: "MU" },
  { name: "Mayotte", code: "YT" },
  { name: "Mexico", code: "MX" },
  { name: "Micronesia, Federated States of", code: "FM" },
  { name: "Moldova, Republic of", code: "MD" },
  { name: "Monaco", code: "MC" },
  { name: "Mongolia", code: "MN" },
  { name: "Montenegro", code: "ME" },
  { name: "Montserrat", code: "MS" },
  { name: "Morocco", code: "MA" },
  { name: "Mozambique", code: "MZ" },
  { name: "Myanmar", code: "MM" },
  { name: "Namibia", code: "NA" },
  { name: "Nauru", code: "NR" },
  { name: "Nepal", code: "NP" },
  { name: "Netherlands", code: "NL" },
  { name: "Netherlands Antilles", code: "AN" },
  { name: "New Caledonia", code: "NC" },
  { name: "New Zealand", code: "NZ" },
  { name: "Nicaragua", code: "NI" },
  { name: "Niger", code: "NE" },
  { name: "Nigeria", code: "NG" },
  { name: "Niue", code: "NU" },
  { name: "Norfolk Island", code: "NF" },
  { name: "Northern Mariana Islands", code: "MP" },
  { name: "Norway", code: "NO" },
  { name: "Oman", code: "OM" },
  { name: "Pakistan", code: "PK" },
  { name: "Palau", code: "PW" },
  { name: "Palestinian Territory, Occupied", code: "PS" },
  { name: "Panama", code: "PA" },
  { name: "Papua New Guinea", code: "PG" },
  { name: "Paraguay", code: "PY" },
  { name: "Peru", code: "PE" },
  { name: "Philippines", code: "PH" },
  { name: "Pitcairn", code: "PN" },
  { name: "Poland", code: "PL" },
  { name: "Portugal", code: "PT" },
  { name: "Puerto Rico", code: "PR" },
  { name: "Qatar", code: "QA" },
  { name: "Reunion", code: "RE" },
  { name: "Romania", code: "RO" },
  { name: "Russian Federation", code: "RU" },
  { name: "RWANDA", code: "RW" },
  { name: "Saint Helena", code: "SH" },
  { name: "Saint Kitts and Nevis", code: "KN" },
  { name: "Saint Lucia", code: "LC" },
  { name: "Saint Pierre and Miquelon", code: "PM" },
  { name: "Saint Vincent and the Grenadines", code: "VC" },
  { name: "Samoa", code: "WS" },
  { name: "San Marino", code: "SM" },
  { name: "Sao Tome and Principe", code: "ST" },
  { name: "Saudi Arabia", code: "SA" },
  { name: "Senegal", code: "SN" },
  { name: "Serbia", code: "RS" },
  { name: "Seychelles", code: "SC" },
  { name: "Sierra Leone", code: "SL" },
  { name: "Singapore", code: "SG" },
  { name: "Slovakia", code: "SK" },
  { name: "Slovenia", code: "SI" },
  { name: "Solomon Islands", code: "SB" },
  { name: "Somalia", code: "SO" },
  { name: "South Africa", code: "ZA" },
  { name: "South Georgia and the South Sandwich Islands", code: "GS" },
  { name: "Spain", code: "ES" },
  { name: "Sri Lanka", code: "LK" },
  { name: "Sudan", code: "SD" },
  { name: "Suriname", code: "SR" },
  { name: "Svalbard and Jan Mayen", code: "SJ" },
  { name: "Swaziland", code: "SZ" },
  { name: "Sweden", code: "SE" },
  { name: "Switzerland", code: "CH" },
  { name: "Syrian Arab Republic", code: "SY" },
  { name: "Taiwan, Province of China", code: "TW" },
  { name: "Tajikistan", code: "TJ" },
  { name: "Tanzania, United Republic of", code: "TZ" },
  { name: "Thailand", code: "TH" },
  { name: "Timor-Leste", code: "TL" },
  { name: "Togo", code: "TG" },
  { name: "Tokelau", code: "TK" },
  { name: "Tonga", code: "TO" },
  { name: "Trinidad and Tobago", code: "TT" },
  { name: "Tunisia", code: "TN" },
  { name: "Turkey", code: "TR" },
  { name: "Turkmenistan", code: "TM" },
  { name: "Turks and Caicos Islands", code: "TC" },
  { name: "Tuvalu", code: "TV" },
  { name: "Uganda", code: "UG" },
  { name: "Ukraine", code: "UA" },
  { name: "United Arab Emirates", code: "AE" },
  { name: "United Kingdom", code: "GB" },
  { name: "United States", code: "US" },
  { name: "United States Minor Outlying Islands", code: "UM" },
  { name: "Uruguay", code: "UY" },
  { name: "Uzbekistan", code: "UZ" },
  { name: "Vanuatu", code: "VU" },
  { name: "Venezuela", code: "VE" },
  { name: "Viet Nam", code: "VN" },
  { name: "Virgin Islands, British", code: "VG" },
  { name: "Virgin Islands, U.S.", code: "VI" },
  { name: "Wallis and Futuna", code: "WF" },
  { name: "Western Sahara", code: "EH" },
  { name: "Yemen", code: "YE" },
  { name: "Zambia", code: "ZM" },
  { name: "Zimbabwe", code: "ZW" }
];

так что у нас может быть список стран в раскрывающемся поле "Страны" в ContactForm.vue.

Затем мы добавляем миксин, на который мы ссылались в ContactForm.vue. Создайте папку mixins в папке src и добавьте файл requestsMixin.js. Там добавьте:

const APIURL = "http://localhost:3000";
const axios = require("axios");
export const requestsMixin = {
  methods: {
    getContacts() {
      return axios.get(`${APIURL}/contacts`);
    },
    addContact(data) {
      return axios.post(`${APIURL}/contacts`, data);
    },
    editContact(data) {
      return axios.put(`${APIURL}/contacts/${data.id}`, data);
    },
    deleteContact(id) {
      return axios.delete(`${APIURL}/contacts/${id}`);
    }
  }
};

Это функции для возврата обещаний для запросов, которые мы делаем к нашей серверной части.

Затем в Home.vue замените существующий код следующим:

<template>
  <div class="page">
    <h1 class="text-center">Address Book</h1>
    <b-button-toolbar>
      <b-button @click="openAddModal()">Add Contact</b-button>
      <b-button @click="getAllContacts()">Refresh</b-button>
    </b-button-toolbar>
    <br />
    <b-table-simple responsive>
      <b-thead>
        <b-tr>
          <b-th>First Name</b-th>
          <b-th>Last Name</b-th>
          <b-th>Address</b-th>
          <b-th>Phone</b-th>
          <b-th>Email</b-th>
          <b-th>Age</b-th>
          <b-th></b-th>
          <b-th></b-th>
        </b-tr>
      </b-thead>
      <b-tbody>
        <b-tr v-for="c in contacts" :key="c.id">
          <b-td>{{c.firstName}}</b-td>
          <b-td>{{c.lastName}}</b-td>
          <b-td>{{c.addressLineOne}}, {{c.city}}, {{c.region}}, {{c.country}}, {{c.postalCode}}</b-td>
          <b-td>{{c.phone}}</b-td>
          <b-td>{{c.email}}</b-td>
          <b-td>{{c.age}}</b-td>
          <b-td>
            <b-button @click="openEditModal(c)">Edit</b-button>
          </b-td>
          <b-td>
            <b-button @click="deleteOneContact(c.id)">Delete</b-button>
          </b-td>
        </b-tr>
      </b-tbody>
    </b-table-simple>
<b-modal id="add-modal" title="Add Contact" hide-footer>
      <ContactForm @saved="closeModal()" @cancelled="closeModal()" :edit="false"></ContactForm>
    </b-modal>
<b-modal id="edit-modal" title="Edit Contact" hide-footer>
      <ContactForm
        @saved="closeModal()"
        @cancelled="closeModal()"
        :edit="true"
        :contact="selectedContact"
      ></ContactForm>
    </b-modal>
  </div>
</template>
<script>
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import { requestsMixin } from "@/mixins/requestsMixin";
import ContactForm from "@/components/ContactForm";
export default {
  name: "home",
  mixins: [requestsMixin],
  components: {
    ContactForm
  },
  computed: {
    contacts() {
      return this.$store.state.contacts;
    }
  },
  beforeMount() {
    this.getAllContacts();
  },
  data() {
    return {
      selectedContact: {}
    };
  },
  methods: {
    openAddModal() {
      this.$bvModal.show("add-modal");
    },
    openEditModal(contact) {
      this.$bvModal.show("edit-modal");
      this.selectedContact = contact;
    },
    closeModal() {
      this.$bvModal.hide("add-modal");
      this.$bvModal.hide("edit-modal");
      this.selectedContact = {};
    },
    async deleteOneContact(id) {
      await this.deleteContact(id);
      this.getAllContacts();
    },
    async getAllContacts() {
      const response = await this.getContacts();
      this.$store.commit("setContacts", response.data);
    }
  }
};
</script>
<style scoped>
#add-button {
  margin-bottom: 20px;
}
</style>

У нас есть таблица для отображения списка контактов из магазина. Этот компонент следит за обновлениями нашего магазина Vuex, получая их из свойства contacts в поле computed. Последние данные магазина Vuex всегда возвращаются туда.

Данные загружаются при первой загрузке страницы с помощью вызова функции getAllContacts в ловушке beforeMount. getAllContacts установить контакты в магазине после получения из серверной части.

У нас есть кнопки для открытия и закрытия модальных окон, содержащих нашу контактную форму. Обратите внимание, что мы должны настроить форму для передачи в опору contact в ContactForm в модальном окне редактирования, установив this.selectedContact в openEditModal с переданным аргументом contact. openEditModal используется кнопкой Edit в каждой строке таблицы.

В каждой строке таблицы также есть кнопка «Удалить», мы передаем туда идентификатор контакта, чтобы мы могли удалить его по идентификатору.

Затем в App.vue мы заменяем существующий код на:

<template>
  <div id="app">
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand href="#">Address Book</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item to="/" :active="path  == '/'">Home</b-nav-item>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>
    <router-view />
  </div>
</template>
<script>
export default {
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  }
};
</script>
<style lang="scss">
.page {
  padding: 20px;
}
button {
  margin-right: 10px;
}
</style>

В этот файл мы добавляем компонент BootstrapVue navbar и выделяем ссылки, проверяя path, который мы получаем в блоке watch. Опора active - это место, где устанавливается подсветка. Если active равно true, ссылка будет выделена. Мы выбираем выделение ссылки, если path совпадает с маршрутом, по которому находится пользователь.

В блоке style мы добавляем отступы на нашу страницу и поля для наших кнопок.

В main.js замените существующий код на:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required, email, min_value, max_value } from "vee-validate/dist/rules";
extend("required", required);
extend("email", email);
extend("min_value", min_value);
extend("max_value", max_value);
extend("phone", {
  validate: (value, { country }) => {
    if (["United States", "Canada"].includes(country)) {
      return /^(\(\d{3}\)|\d{3})-?\d{3}-?\d{4}$/.test(value);
    }
    return true;
  },
  message: "Phone number is invalid.",
  params: [{ name: "country", isTarget: true }]
});
extend("postal_code", {
  validate: (value, { country }) => {
    if ("United States" == country) {
      return /^[0-9]{5}(?:-[0-9]{4})?$/.test(value);
    } else if ("Canada" == country) {
      return /^[A-Za-z]\d[A-Za-z][ -]?\d[A-Za-z]\d$/.test(value);
    }
    return true;
  },
  message: "Phone number is invalid.",
  params: [{ name: "country", isTarget: true }]
});
Vue.config.productionTip = false;
Vue.use(BootstrapVue);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
new Vue({
  router,
  store,
  render: h => h(App),
  mounted() {
    this.$router.push("/");
  }
}).$mount("#app");

У нас есть правила проверки Vee-Validate, добавленные здесь, и мы регистрируем наши компоненты BootstrapVue и компоненты проверки Vee-Validate здесь, чтобы мы могли использовать их в наших шаблонах.

Обратите внимание, что у нас есть:

mounted() {
  this.$router.push("/");
}

в объекте, переданном в конструктор Vue, чтобы наше созданное приложение для Windows не отображало пустую страницу.

В router.js мы заменяем существующий код на:

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
Vue.use(Router);
export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    }
  ]
});

позволить нам перейти к Home.vue.

В store.js замените существующий код на:

import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
  state: {
    contacts: []
  },
  mutations: {
    setContacts(state, payload) {
      state.contacts = payload;
    }
  },
  actions: {}
});

так что мы можем хранить контакты в магазине для быстрого доступа всех компонентов.

Теперь мы можем запустить npm run electron:serve, чтобы запустить приложение.

Чтобы запустить серверную часть, мы сначала устанавливаем пакет json-server, запустив npm i json-server. Затем перейдите в папку нашего проекта и запустите:

json-server --watch db.json

В db.json измените текст на:

{
  "contacts": [
  ]
}

Итак, у нас есть contacts конечные точки, определенные в requests.js.

В итоге имеем следующее: