finish account

This commit is contained in:
liangping 2021-08-13 23:08:04 +08:00
parent 20a1ea7ca0
commit fea3980009
12 changed files with 771 additions and 23 deletions

View File

@ -49,6 +49,7 @@
"vue-echarts": "5.0.0-beta.0", "vue-echarts": "5.0.0-beta.0",
"vue-feather-icons": "5.1.0", "vue-feather-icons": "5.1.0",
"vue-flatpickr-component": "8.1.6", "vue-flatpickr-component": "8.1.6",
"vue-flex-waterfall": "^1.0.7",
"vue-form-wizard": "0.8.4", "vue-form-wizard": "0.8.4",
"vue-i18n": "8.22.2", "vue-i18n": "8.22.2",
"vue-loader": "^15.9.6", "vue-loader": "^15.9.6",
@ -67,6 +68,7 @@
"vuex": "3.6.0" "vuex": "3.6.0"
}, },
"devDependencies": { "devDependencies": {
"@cosmjs/launchpad": "^0.25.6",
"@vue/cli-plugin-babel": "~4.5.9", "@vue/cli-plugin-babel": "~4.5.9",
"@vue/cli-plugin-eslint": "~4.5.9", "@vue/cli-plugin-eslint": "~4.5.9",
"@vue/cli-plugin-router": "~4.5.9", "@vue/cli-plugin-router": "~4.5.9",

View File

@ -61,14 +61,65 @@
<dark-Toggler class="d-none d-lg-block" /> <dark-Toggler class="d-none d-lg-block" />
<search-bar /> <search-bar />
<locale /> <locale />
<b-dropdown
class="ml-1"
variant="link"
no-caret
toggle-class="p-0"
right
>
<template #button-content>
<b-button
v-ripple.400="'rgba(255, 255, 255, 0.15)'"
variant="primary"
class="btn-icon"
>
<feather-icon icon="UserIcon" />
</b-button>
</template>
<b-dropdown-item :to="{ name: 'portfolio' }">
<feather-icon
icon="PieChartIcon"
size="16"
/>
<span class="align-middle ml-50">Portofolio</span>
</b-dropdown-item>
<b-dropdown-item :to="{ name: 'accounts' }">
<feather-icon
icon="KeyIcon"
size="16"
/>
<span class="align-middle ml-50">Accounts</span>
</b-dropdown-item>
<b-dropdown-item :to="{ name: 'addresses' }">
<feather-icon
icon="BookOpenIcon"
size="16"
/>
<span class="align-middle ml-50">Address Book</span>
</b-dropdown-item>
<b-dropdown-item :to="{ name: 'setting' }">
<feather-icon
icon="SettingsIcon"
size="16"
/>
<span class="align-middle ml-50">Setting</span>
</b-dropdown-item>
</b-dropdown>
</b-navbar-nav> </b-navbar-nav>
</div> </div>
</template> </template>
<script> <script>
import { import {
BLink, BNavbarNav, BMedia, BMediaAside, BAvatar, BMediaBody, VBTooltip, BLink, BNavbarNav, BMedia, BMediaAside, BAvatar, BMediaBody, VBTooltip, BButton, BDropdown, BDropdownItem,
} from 'bootstrap-vue' } from 'bootstrap-vue'
import Ripple from 'vue-ripple-directive'
import DarkToggler from '@core/layouts/components/app-navbar/components/DarkToggler.vue' import DarkToggler from '@core/layouts/components/app-navbar/components/DarkToggler.vue'
import Locale from '@core/layouts/components/app-navbar/components/Locale.vue' import Locale from '@core/layouts/components/app-navbar/components/Locale.vue'
import SearchBar from '@core/layouts/components/app-navbar/components/SearchBar.vue' import SearchBar from '@core/layouts/components/app-navbar/components/SearchBar.vue'
@ -84,6 +135,9 @@ export default {
BMedia, BMedia,
BMediaAside, BMediaAside,
BMediaBody, BMediaBody,
BButton,
BDropdown,
BDropdownItem,
// Navbar Components // Navbar Components
DarkToggler, DarkToggler,
@ -94,6 +148,7 @@ export default {
}, },
directives: { directives: {
'b-tooltip': VBTooltip, 'b-tooltip': VBTooltip,
Ripple,
}, },
props: { props: {
toggleVerticalMenuActive: { toggleVerticalMenuActive: {
@ -108,9 +163,6 @@ export default {
}, },
computed: { computed: {
selected_chain() { selected_chain() {
// const c = this.$route.params.chain
// const has = Object.keys(store.state.chains.config).findIndex(i => i === c)
// const selected = (has > -1) ? store.state.chains.config[c] : store.state.chains.config.cosmos
return store.state.chains.selected return store.state.chains.selected
}, },
}, },

View File

@ -12,6 +12,22 @@ dayjs.extend(localeData)
dayjs.extend(duration) dayjs.extend(duration)
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
export function getLocalObject(name) {
const text = localStorage.getItem(name)
if (text) {
return JSON.parse(text)
}
return null
}
export function getLocalChains() {
return getLocalObject('chains')
}
export function getLocalAccounts() {
return getLocalObject('accounts')
}
export function toDuration(value) { export function toDuration(value) {
return dayjs.duration(value).humanize() return dayjs.duration(value).humanize()
} }
@ -76,25 +92,33 @@ export function isToken(value) {
return is return is
} }
export function formatToken(token) { export function formatTokenDenom(tokenDenom) {
if (token) { if (tokenDenom) {
let denom = token.denom.toUpperCase() let denom = tokenDenom.toUpperCase()
if (denom.charAt(0) === 'U') { if (denom.charAt(0) === 'U') {
denom = denom.substring(1) denom = denom.substring(1)
const amount = token.amount / 1000000 } else if (denom === 'BASECRO') {
denom = 'CRO'
}
return denom
}
return tokenDenom
}
export function formatTokenAmount(tokenAmount, fraction = 2, denom = 'uatom') {
if (denom.startsWith('u')) {
// for special case
}
const amount = tokenAmount / 1000000
if (amount > 10) { if (amount > 10) {
return `${parseFloat(amount.toFixed())} ${denom}` return parseFloat(amount.toFixed(fraction))
} }
return `${parseFloat(amount)} ${denom}` return parseFloat(amount)
} }
if (denom === 'BASECRO') {
const amount = token.amount / 1000000 export function formatToken(token) {
if (amount > 10) { if (token) {
return `${parseFloat(amount.toFixed())} CRO` return `${formatTokenAmount(token.amount, 2, token.denom)} ${formatTokenDenom(token.denom)}`
}
return `${parseFloat(amount)} CRO`
}
return `${parseFloat(token.amount)} ${denom}`
} }
return token return token
} }

View File

@ -188,6 +188,38 @@ const chainAPI = class ChainFetch {
const ret = await fetch(this.config.api + url).then(response => response.json()) const ret = await fetch(this.config.api + url).then(response => response.json())
return ret return ret
} }
static fetch(host, url) {
const ret = fetch(host + url).then(response => response.json())
return ret
}
static async getAuthAccount(baseurl, address) {
return ChainFetch.fetch(baseurl, '/auth/accounts/'.concat(address)).then(data => commonProcess(data))
}
static async getBankBalance(baseurl, address) {
return ChainFetch.fetch(baseurl, '/bank/balances/'.concat(address)).then(data => commonProcess(data))
}
static async getIBCDenomTrace(baseurl, hash) {
const h = hash.substring(hash.indexOf('/'))
return ChainFetch.fetch(baseurl, '/ibc/applications/transfer/v1beta1/denom_traces/'.concat(h)).then(data => commonProcess(data))
}
static async getIBCDenomTraceText(baseurl, hash) {
return ChainFetch.getIBCDenomTrace(baseurl, hash).then(res => res.denom_trace.base_denom)
}
// CoinMarketCap
static async fetchCoinMarketCap(url) {
const host = 'https://price.ping.pub'
return fetch(host + url).then(response => response.json())
}
static async fetchTokenQuote(symbol) {
return ChainFetch.fetchCoinMarketCap(`/quote/${symbol}`)
}
} }
export default chainAPI export default chainAPI

View File

@ -26,6 +26,62 @@ const router = new VueRouter({
], ],
}, },
}, },
{
path: '/user/setting',
name: 'setting',
component: () => import('@/views/UserSetting.vue'),
meta: {
pageTitle: 'Setting',
breadcrumb: [
{
text: 'Setting',
active: true,
},
],
},
},
{
path: '/user/portfolio',
name: 'portfolio',
component: () => import('@/views/UserPortfolio.vue'),
meta: {
pageTitle: 'Portfolio',
breadcrumb: [
{
text: 'Portfolio',
active: true,
},
],
},
},
{
path: '/user/accounts',
name: 'accounts',
component: () => import('@/views/UserAccounts.vue'),
meta: {
pageTitle: 'Accounts',
breadcrumb: [
{
text: 'Accounts',
active: true,
},
],
},
},
{
path: '/user/address',
name: 'addresses',
component: () => import('@/views/UserAddressBook.vue'),
meta: {
pageTitle: 'Address Book',
breadcrumb: [
{
text: 'Transaction',
active: true,
},
],
},
},
// chain modules // chain modules
{ {
path: '/:chain/', path: '/:chain/',
@ -189,7 +245,7 @@ const router = new VueRouter({
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const c = to.params.chain const c = to.params.chain
store.commit('select', { chain_name: c }) if (c) store.commit('select', { chain_name: c })
const config = JSON.parse(localStorage.getItem('chains')) const config = JSON.parse(localStorage.getItem('chains'))
// const has = Object.keys(config).findIndex(i => i === c) // const has = Object.keys(config).findIndex(i => i === c)

View File

@ -0,0 +1,315 @@
<template>
<div>
<form-wizard
color="#7367F0"
:title="null"
:subtitle="null"
shape="square"
finish-button-text="Submit"
back-button-text="Previous"
@on-complete="formSubmitted"
>
<!-- Device tab -->
<tab-content
title="Device"
:before-change="validationFormDevice"
>
<validation-observer
ref="deviceRules"
tag="form"
>
<b-row>
<b-col md="12">
<b-form-group
label="Select a device to import accounts"
label-for="device"
>
<validation-provider
#default="{ errors }"
name="device"
rules="required"
>
<div class="demo-inline-spacing">
<b-form-radio
v-model="device"
name="device"
value="keplr"
checked
>
Keplr
</b-form-radio>
<b-form-radio
v-model="device"
name="device"
value="ledger"
disabled
>
Ledger Nano
</b-form-radio>
<b-form-radio
v-model="device"
name="device"
value="nmemonic"
disabled
>
Nmemonic
</b-form-radio>
</div>
<small class="text-danger">{{ errors[0] }}</small>
</validation-provider>
</b-form-group>
</b-col>
</b-row>
</validation-observer>
</tab-content>
<!-- address -->
<tab-content
title="Accounts"
:before-change="validationFormAddress"
>
<validation-observer
ref="accountRules"
tag="form"
>
<b-row>
<b-col md="12">
<b-form-group
label="Account Name"
label-for="account_name"
>
<validation-provider
#default="{ errors }"
name="Account Name"
rules="required"
>
<b-form-input
id="account_name"
v-model="name"
:state="errors.length > 0 ? false:null"
placeholder="Keplr"
/>
<small class="text-danger">{{ errors[0] }}</small>
</validation-provider>
</b-form-group>
</b-col>
<b-col md="12">
<b-form-group
label="Import Address For Chains:"
>
<validation-provider
#default="{ errors }"
name="addrs"
rules="required"
>
<div class="demo-inline-spacing text-uppercase">
<b-row>
<b-col
v-for="item, key in chains"
:key="key"
cols="3"
class="mb-25"
>
<b-form-checkbox
v-model="selected"
name="addrs"
:value="key"
>
<b-avatar
:src="item.logo"
size="18"
variant="light-primary"
rounded=""
/>
{{ item.chain_name }}
</b-form-checkbox>
</b-col>
</b-row>
</div>
<small class="text-danger">{{ errors[0] }}</small>
</validation-provider>
</b-form-group>
</b-col>
</b-row>
</validation-observer>
</tab-content>
<tab-content
title="Confirmation"
>
<div class="d-flex border-bottom mb-2">
<feather-icon
icon="UserIcon"
size="19"
class="mb-50"
/>
<h4 class="mb-0 ml-50">
{{ name }}
</h4>
</div>
<b-row class="mb-2">
<b-col
v-for="i in addresses"
:key="i.addr"
cols="12"
>
<b-input-group class="mb-25">
<b-input-group-prepend is-text>
<b-avatar
:src="i.logo"
size="18"
variant="light-primary"
rounded
/>
</b-input-group-prepend>
<b-form-input :value="i.addr" />
</b-input-group>
</b-col>
</b-row>
</tab-content>
</form-wizard>
</div>
</template>
<script>
import { FormWizard, TabContent } from 'vue-form-wizard'
import { ValidationProvider, ValidationObserver } from 'vee-validate'
import ToastificationContent from '@core/components/toastification/ToastificationContent.vue'
// import 'vue-form-wizard/dist/vue-form-wizard.min.css'
import 'vue-form-wizard/dist/vue-form-wizard.min.css'
import {
BRow,
BCol,
BFormGroup,
BFormInput,
BFormRadio,
BFormCheckbox,
BAvatar,
BInputGroup,
BInputGroupPrepend,
} from 'bootstrap-vue'
import { required } from '@validations'
import store from '@/store'
import { addressDecode, addressEnCode } from '@/libs/data'
import { Bech32 } from '@cosmjs/encoding'
export default {
components: {
ValidationProvider,
ValidationObserver,
FormWizard,
TabContent,
BAvatar,
BRow,
BCol,
BFormGroup,
BFormInput,
BFormRadio,
BFormCheckbox,
BInputGroup,
BInputGroupPrepend,
// eslint-disable-next-line vue/no-unused-components
ToastificationContent,
},
data() {
return {
device: 'keplr',
name: '',
options: {},
required,
selected: [],
accounts: null,
}
},
computed: {
chains() {
const config = JSON.parse(localStorage.getItem('chains'))
return config
},
addresses() {
if (!this.accounts) return []
const { data } = addressDecode(this.accounts[0].address)
return this.selected.map(x => {
const { logo, addr_prefix } = this.chains[x]
const addr = addressEnCode(addr_prefix, data)
return { chain: x, addr, logo }
})
},
},
created() {
const { selected } = store.state.chains
if (selected && selected.chain_name) {
this.selected.push(selected.chain_name)
}
},
methods: {
async cennectKeplr() {
if (!window.getOfflineSigner || !window.keplr) {
// eslint-disable-next-line no-alert
alert('Please install keplr extension')
return null
}
const chainId = 'cosmoshub'
await window.keplr.enable(chainId)
const offlineSigner = window.getOfflineSigner(chainId)
return offlineSigner.getAccounts()
},
formSubmitted() {
const string = localStorage.getItem('accounts')
const accounts = string ? JSON.parse(string) : {}
accounts[this.name] = {
name: this.name,
device: this.device,
address: this.addresses,
}
localStorage.setItem('accounts', JSON.stringify(accounts))
this.$toast({
component: ToastificationContent,
props: {
title: 'Address Saved!',
icon: 'EditIcon',
variant: 'success',
},
})
},
async validationFormDevice() {
await this.cennectKeplr().then(accounts => {
if (accounts) {
this.accounts = accounts
const key = Bech32.decode(accounts[0].address)
console.log(accounts, key)
}
})
return new Promise((resolve, reject) => {
this.$refs.deviceRules.validate().then(success => {
if (success) {
resolve(true)
}
reject()
})
})
},
validationFormAddress() {
return new Promise((resolve, reject) => {
this.$refs.accountRules.validate().then(success => {
if (success) {
resolve(true)
} else {
reject()
}
})
})
},
},
}
</script>
<style lang="scss">
@import '@core/scss/vue/pages/ui-feather.scss';
@import '@core/scss/vue/libs/vue-wizard.scss';
</style>

View File

@ -1,7 +1,7 @@
<template> <template>
<b-card v-if="assets"> <b-card v-if="assets">
<b-card-title> <b-card-title>
Onchain Assets Assets
</b-card-title> </b-card-title>
<b-table <b-table
:items="assets" :items="assets"

227
src/views/UserAccounts.vue Normal file
View File

@ -0,0 +1,227 @@
<template>
<div class="text-center">
<b-tabs
pills
active-nav-item-class="font-weight-bolder"
>
<b-tab
v-for="item,index in accounts"
:key="index"
>
<template #title>
<feather-icon icon="UserIcon" />
<span>{{ item.name }}</span>
</template>
<b-row>
<b-col
v-for="acc, j in item.address"
:key="j"
sm="12"
md="6"
xl="4"
:class="(balances[acc.addr])? 'order-1' : 'order-9' "
>
<b-card
no-body
class="card-browser-states text-truncate"
>
<b-card-header>
<div>
<b-card-title> <span class="text-uppercase">{{ acc.chain }}</span></b-card-title>
</div>
<feather-icon
icon="MoreVerticalIcon"
size="18"
class="cursor-pointer"
/>
</b-card-header>
<b-card-body>
<b-row>
<b-col>
<div class="d-flex justify-content-between">
<b-avatar
:src="acc.logo"
size="28"
variant="light-primary"
rounded
/>
<h3>${{ formatBalance(balances[acc.addr]) }}</h3>
</div>
<small class="pl-1 float-right text-muted text-overflow ">
{{ acc.addr }}
</small>
</b-col>
</b-row>
<b-row v-if="balances[acc.addr]">
<b-col>
<b-tabs
active-nav-item-class="font-weight-bold text-second"
>
<b-tab title="Assets">
<div
v-for="b,i in balances[acc.addr]"
:key="i"
class="d-flex justify-content-between align-items-center"
>
<div class="ml-25 font-weight-bolder text-uppercase">
{{ formatDenom(b.denom) }}
</div>
<div class="d-flex flex-column text-right">
<span class="font-weight-bold mb-0">{{ formatAmount(b.amount) }}</span>
<span class="font-small-2 text-muted text-nowrap">${{ formatCurrency(b.amount, b.denom) }}</span>
</div>
</div>
</b-tab>
</b-tabs>
</b-col>
</b-row>
</b-card-body>
</b-card>
</b-col>
</b-row>
</b-tab>
</b-tabs>
<b-card
v-b-modal.modal-center
class="addzone"
>
<feather-icon icon="PlusIcon" />
Import Accounts
</b-card>
<!-- modal vertical center -->
<b-modal
id="modal-center"
centered
size="lg"
title="Add Account"
hide-footer
hide-header-close
cancel-disabled
scrollable
>
<form-wizard-number />
</b-modal>
</div>
</template>
<script>
import chainAPI from '@/libs/fetch'
import {
BCard, BCardHeader, BCardTitle, BCardBody, VBModal, BRow, BCol, BTabs, BTab, BAvatar,
} from 'bootstrap-vue'
import Ripple from 'vue-ripple-directive'
import FeatherIcon from '@/@core/components/feather-icon/FeatherIcon.vue'
import {
formatTokenAmount, formatTokenDenom, getLocalAccounts, getLocalChains,
} from '@/libs/data'
import FormWizardNumber from './FormWizardNumber.vue'
// import { SigningCosmosClient } from '@cosmjs/launchpad'
export default {
components: {
BAvatar,
BCard,
BRow,
BCol,
BTabs,
BTab,
BCardHeader,
BCardBody,
BCardTitle,
FormWizardNumber,
FeatherIcon,
},
directives: {
'b-modal': VBModal,
Ripple,
},
data() {
return {
accounts: [],
balances: {},
ibcDenom: {},
quotes: {},
}
},
created() {
this.accounts = getLocalAccounts()
const chains = getLocalChains()
if (this.accounts) {
Object.keys(this.accounts).forEach(acc => {
this.accounts[acc].address.forEach(add => {
chainAPI.getBankBalance(chains[add.chain].api, add.addr).then(res => {
if (res && res.length > 0) {
this.$set(this.balances, add.addr, res)
res.forEach(token => {
let symbol
if (token.denom.startsWith('ibc')) {
chainAPI.getIBCDenomTraceText(chains[add.chain].api, token.denom).then(denom => {
this.$set(this.ibcDenom, token.denom, denom)
symbol = formatTokenDenom(denom)
})
} else {
symbol = formatTokenDenom(token.denom)
}
if (symbol) {
if (!this.quotes[symbol]) {
chainAPI.fetchTokenQuote(symbol).then(quote => {
this.$set(this.quotes, symbol, quote)
})
}
}
})
}
})
})
})
}
},
methods: {
formatDenom(v) {
const denom = (v.startsWith('ibc') ? this.ibcDenom[v] : v)
return formatTokenDenom(denom)
},
formatAmount(v) {
return formatTokenAmount(v)
},
formatCurrency(amount, denom) {
const qty = this.formatAmount(amount)
const d2 = this.formatDenom(denom)
const userCurrency = 'USD'
const quote = this.quotes[d2]
if (quote && quote.quote) {
const { price } = quote.quote[userCurrency]
return parseFloat((qty * price).toFixed(2))
}
return 0
},
formatBalance(v) {
if (v) {
const ret = v.map(x => this.formatCurrency(x.amount, x.denom)).reduce((t, c) => t + c)
return parseFloat(ret.toFixed(2))
}
return 0
},
},
}
</script>
<style lang="css">
.addzone {
border: 2px dashed #ced4da;
background: #fff;
border-radius: 6px;
cursor: pointer;
box-shadow: none;
}
.addzone :hover {
border: 2px dashed #7367F0;
}
</style>

View File

@ -0,0 +1,11 @@
<template>
<div class="" />
</template>
<script>
</script>
<style scoped>
</style>

View File

11
src/views/UserSetting.vue Normal file
View File

@ -0,0 +1,11 @@
<template>
<div class="" />
</template>
<script>
</script>
<style scoped>
</style>

View File

@ -950,6 +950,19 @@
bech32 "^1.1.4" bech32 "^1.1.4"
readonly-date "^1.0.0" readonly-date "^1.0.0"
"@cosmjs/launchpad@^0.25.6":
version "0.25.6"
resolved "https://registry.yarnpkg.com/@cosmjs/launchpad/-/launchpad-0.25.6.tgz#c75f5d21be57af55fcb892f929520fa97f2d5bcc"
integrity sha512-4Yhn4cX50UE6jZz/hWqKeeCmvrlrz0BBwOdYX/29k25FqP+oLAow1xKm6UxgYuuAq8Pg/bUvswxSqwegZJTb6g==
dependencies:
"@cosmjs/amino" "^0.25.6"
"@cosmjs/crypto" "^0.25.6"
"@cosmjs/encoding" "^0.25.6"
"@cosmjs/math" "^0.25.6"
"@cosmjs/utils" "^0.25.6"
axios "^0.21.1"
fast-deep-equal "^3.1.3"
"@cosmjs/math@^0.25.6": "@cosmjs/math@^0.25.6":
version "0.25.6" version "0.25.6"
resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.25.6.tgz#25c7b106aaded889a5b80784693caa9e654b0c28" resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.25.6.tgz#25c7b106aaded889a5b80784693caa9e654b0c28"
@ -2304,7 +2317,7 @@ axios-mock-adapter@1.19.0:
fast-deep-equal "^3.1.3" fast-deep-equal "^3.1.3"
is-buffer "^2.0.3" is-buffer "^2.0.3"
axios@0.21.1: axios@0.21.1, axios@^0.21.1:
version "0.21.1" version "0.21.1"
resolved "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz" resolved "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz"
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
@ -11061,6 +11074,11 @@ vue-flatpickr-component@8.1.6:
dependencies: dependencies:
flatpickr "^4.6.6" flatpickr "^4.6.6"
vue-flex-waterfall@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/vue-flex-waterfall/-/vue-flex-waterfall-1.0.7.tgz#843597024eb3f880a03b949baad1f6b4be9f68ff"
integrity sha512-YaxIUEvBQAOwBMastaJ8z5gFZFa+pZHHrpoTcZxxfCRZUdOxFBjLoyzCP9q+T8WlAhjeHD+Zsaeor9kKzRpccg==
vue-form-wizard@0.8.4: vue-form-wizard@0.8.4:
version "0.8.4" version "0.8.4"
resolved "https://registry.npmjs.org/vue-form-wizard/-/vue-form-wizard-0.8.4.tgz" resolved "https://registry.npmjs.org/vue-form-wizard/-/vue-form-wizard-0.8.4.tgz"