Копирование в буфер обмена - популярная функция для веб-приложений, таких как менеджеры паролей, где людям неудобно выделять текст, а затем копировать его. Эту функцию легко добавить в собственное веб-приложение.

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

Начиная

Для начала создадим проект, запустив npx @vue/cli create password-manager. В мастере выберите «Выбрать функции вручную» и выберите включение Babel, Vue Router и Vuex в наше приложение.

Далее мы устанавливаем необходимые нам библиотеки. Нам нужны Axios для выполнения HTTP-запросов, Bootstrap Vue для стилизации, V-Clipboard для копирования в буфер обмена и Vee-Validate для проверки формы. Устанавливаем их, запустив:

npm i axios bootstrap-vue v-clipboard vee-validate

После установки библиотек мы можем приступить к созданию приложения. Сначала в папке components создайте файл с именем PasswordForm.vue для нашей формы пароля. Затем мы добавляем:

<template>
  <ValidationObserver ref="observer" v-slot="{ invalid }">
    <b-form @submit.prevent="onSubmit" novalidate>
      <b-form-group label="Name">
        <ValidationProvider name="name" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.name"
            required
            placeholder="Name"
            name="name"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Name is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-form-group label="URL">
        <ValidationProvider name="url" rules="required|url" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.url"
            required
            placeholder="URL"
            name="url"
          ></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="Username">
        <ValidationProvider name="username" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.username"
            required
            placeholder="Username"
            name="username"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Username is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-form-group label="Password">
        <ValidationProvider name="password" rules="required" v-slot="{ errors }">
          <b-form-input
            type="password"
            :state="errors.length == 0"
            v-model="form.password"
            required
            placeholder="Password"
            name="password"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Password is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-button type="submit" variant="primary" style="margin-right: 10px">Submit</b-button>
      <b-button type="reset" variant="danger" @click="cancel()">Cancel</b-button>
    </b-form>
  </ValidationObserver>
</template>
<script>
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
  name: "PasswordForm",
  mixins: [requestsMixin],
  props: {
    edit: Boolean,
    password: Object
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
if (this.edit) {
        await this.editPassword(this.form);
      } else {
        await this.addPassword(this.form);
      }
      const response = await this.getPasswords();
      this.$store.commit("setPasswords", response.data);
      this.$emit("saved");
    },
    cancel() {
      this.$emit("cancelled");
    }
  },
  data() {
    return {
      form: {}
    };
  },
  watch: {
    password: {
      handler(p) {
        this.form = JSON.parse(JSON.stringify(p || {}));
      },
      deep: true,
      immediate: true
    }
  }
};
</script>

У нас есть форма пароля в этом компоненте. Форма включает поля имени, URL, имени пользователя и пароля. Все они необходимы. Мы используем Vee-Validate для проверки полей формы. Компонент ValidationObserver предназначен для проверки всей формы, а компонент ValidationProvider - для проверки полей формы, которые он обтекает.

Правило проверки определяется атрибутом rule каждого поля. У нас есть специальное url правило для поля URL. Мы показываем сообщения об ошибках проверки, когда объект errors из слота области имеет ненулевую длину. Опора state предназначена для установки состояния проверки, которое показывает зеленый цвет, когда errors имеет длину 0, и красный цвет в противном случае. Сообщения об ошибках отображаются в компоненте b-form-invalid-feedback.

Когда пользователь нажимает кнопку «Сохранить», вызывается функция onSubmit. Мы получаем состояние проверки формы с помощью using this.$refs.observer.validate();. Ссылка ссылается на ссылку ValidationObserver. Если он принимает значение true, мы вызываем addPassword или editPassword для сохранения записи в зависимости от свойства edit. Затем мы получаем пароли, вызывая getPasswords, а затем помещаем их в наш магазин Vuex, отправляя мутацию setPasswords. Затем мы генерируем событие saved, чтобы закрыть модальное окно на домашней странице.

У нас есть блок watch, который в основном используется при редактировании существующей записи, мы получаем свойство password и устанавливаем его в this.form, создавая копию свойства, чтобы мы обновляли только объект form и ничего, когда данные связываются.

Затем мы создаем папку mixins и добавляем в нее requestsMixin.js. В файле добавить:

const APIURL = "http://localhost:3000";
const axios = require("axios");
export const requestsMixin = {
  methods: {
    getPasswords() {
      return axios.get(`${APIURL}/passwords`);
    },
    addPassword(data) {
      return axios.post(`${APIURL}/passwords`, data);
    },
    editPassword(data) {
      return axios.put(`${APIURL}/passwords/${data.id}`, data);
    },
    deletePassword(id) {
      return axios.delete(`${APIURL}/passwords/${id}`);
    }
  }
};

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

Копировать в буфер обмена

Чтобы скопировать кнопки имени пользователя и пароля, мы используем директиву v-clipboard, которая позволяет нам копировать имя пользователя и пароль соответственно в буфер обмена при нажатии кнопки.

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

<template>
  <div class="page">
    <h1 class="text-center">Password Manager</h1>
    <b-button-toolbar>
      <b-button @click="openAddModal()">Add Password</b-button>
    </b-button-toolbar>
    <br />
    <b-table-simple responsive>
      <b-thead>
        <b-tr>
          <b-th>Name</b-th>
          <b-th>URL</b-th>
          <b-th>Username</b-th>
          <b-th>Password</b-th>
          <b-th></b-th>
          <b-th></b-th>
          <b-th></b-th>
          <b-th></b-th>
        </b-tr>
      </b-thead>
      <b-tbody>
        <b-tr v-for="p in passwords" :key="p.id">
          <b-td>{{p.name}}</b-td>
          <b-td>{{p.url}}</b-td>
          <b-td>{{p.username}}</b-td>
          <b-td>******</b-td>
          <b-td>
            <b-button v-clipboard="() => p.username">Copy Username</b-button>
          </b-td>
          <b-td>
            <b-button v-clipboard="() => p.password">Copy Password</b-button>
          </b-td>
          <b-td>
            <b-button @click="openEditModal(p)">Edit</b-button>
          </b-td>
          <b-td>
            <b-button @click="deleteOnePassword(p.id)">Delete</b-button>
          </b-td>
        </b-tr>
      </b-tbody>
    </b-table-simple>
<b-modal id="add-modal" title="Add Password" hide-footer>
      <PasswordForm @saved="closeModal()" @cancelled="closeModal()" :edit="false"></PasswordForm>
    </b-modal>
<b-modal id="edit-modal" title="Edit Password" hide-footer>
      <PasswordForm
        @saved="closeModal()"
        @cancelled="closeModal()"
        :edit="true"
        :password="selectedPassword"
      ></PasswordForm>
    </b-modal>
  </div>
</template>
<script>
import { requestsMixin } from "@/mixins/requestsMixin";
import PasswordForm from "@/components/PasswordForm";
export default {
  name: "home",
  components: {
    PasswordForm
  },
  mixins: [requestsMixin],
  computed: {
    passwords() {
      return this.$store.state.passwords;
    }
  },
  beforeMount() {
    this.getAllPasswords();
  },
  data() {
    return {
      selectedPassword: {}
    };
  },
  methods: {
    openAddModal() {
      this.$bvModal.show("add-modal");
    },
    openEditModal(password) {
      this.$bvModal.show("edit-modal");
      this.selectedPassword = password;
    },
    closeModal() {
      this.$bvModal.hide("add-modal");
      this.$bvModal.hide("edit-modal");
      this.selectedPassword = {};
    },
    async deleteOnePassword(id) {
      await this.deletePassword(id);
      this.getAllPasswords();
    },
    async getAllPasswords() {
      const response = await this.getPasswords();
      this.$store.commit("setPasswords", response.data);
    }
  }
};
</script>

В этом файле у нас есть таблица для отображения списка паролей, позволяющая пользователям открывать и закрывать модальные окна добавления / редактирования. У нас есть кнопки в каждой строке, чтобы скопировать имя пользователя и пароли, а также позволить пользователям редактировать или удалять каждую запись.

В разделе scripts у нас есть ловушка beforeMount для получения всех паролей во время загрузки страницы с помощью функции getPasswords, которую мы написали в нашем миксине. При нажатии кнопки «Изменить» устанавливается переменная selectedPassword, и мы передаем ее в PasswordForm для редактирования.

Чтобы удалить пароль, мы вызываем deletePassword в нашем миксине, чтобы запросить серверную часть.

Завершение приложения

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

<template>
  <div id="app">
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand href="#">Password Manager</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>

Это добавляет панель навигации Bootstrap в верхнюю часть наших страниц и router-view для отображения определяемых нами маршрутов.

Затем в 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 Clipboard from "v-clipboard";
import { required } from "vee-validate/dist/rules";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
extend("required", required);
extend("url", {
  validate: value => {
    return /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/.test(
      value
    );
  },
  message: "URL is invalid."
});
Vue.use(BootstrapVue);
Vue.use(Clipboard);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.config.productionTip = false;
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

Чтобы добавить библиотеки, которые мы установили в наше приложение, чтобы мы могли использовать их в наших компонентах. Мы добавляем сюда библиотеку V-Clipboard, чтобы мы могли использовать ее на нашей домашней странице. Мы вызываем extend из Vee-Validate, чтобы добавить правила проверки формы, которые мы хотим использовать. Кроме того, мы импортировали CSS Bootstrap в этот файл, чтобы получить стили.

В 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
    }
  ]
})

чтобы включить только нашу домашнюю страницу.

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

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

Это добавляет наше состояние passwords в хранилище, чтобы мы могли наблюдать его в блоке computed компонентов PasswordForm и HomePage. У нас есть функция setPasswords для обновления состояния passwords, и мы используем ее в компонентах путем вызова this.$store.commit(“setPasswords”, response.data);, как мы делали это в PasswordForm.

После всей тяжелой работы мы можем запустить наше приложение, запустив npm run serve.

Демо-бэкэнд

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

json-server --watch db.json

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

{
  "passwords": [
  ]
}

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

После всей кропотливой работы получаем: