This commit is contained in:
saplf
2018-09-29 21:33:23 +08:00
parent 0a2c4e30de
commit be7755c0f3
40 changed files with 936 additions and 73 deletions

View File

@@ -1,3 +1,3 @@
> 1%
last 2 versions
not ie <= 8
not ie <= 10

2
.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
dist/*
node_modules/*

View File

@@ -2,6 +2,7 @@ module.exports = {
root: true,
env: {
browser: true,
node: true,
},
'extends': [
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
registry=http://101.132.121.111:6842

View File

@@ -1,5 +1,21 @@
module.exports = {
presets: [
'@vue/app',
'presets': [
[
'@vue/app',
{
'useBuiltIns': 'entry',
},
],
],
'plugins': [
[
'transform-imports',
{
'vuetify': {
'transform': 'vuetify/es5/components/${member}', // eslint-disable-line no-template-curly-in-string
'preventFullImport': true,
},
},
],
],
}

View File

@@ -9,8 +9,15 @@
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"axios": "^0.18.0",
"lodash": "^4.17.11",
"material-design-icons-iconfont": "^3.0.3",
"tic.common": "^0.1.4",
"vue": "^2.5.17",
"vue-axios": "^2.1.3",
"vue-i18n": "^8.1.0",
"vue-router": "^3.0.1",
"vuetify": "^1.2.5",
"vuex": "^3.0.1"
},
"devDependencies": {
@@ -22,8 +29,12 @@
"@vue/test-utils": "^1.0.0-beta.20",
"babel-core": "7.0.0-bridge.0",
"babel-jest": "^23.0.1",
"babel-plugin-transform-imports": "^1.5.1",
"node-sass": "^4.9.0",
"sass-loader": "^7.0.1",
"vue-template-compiler": "^2.5.17"
"stylus": "^0.54.5",
"stylus-loader": "^3.0.2",
"vue-template-compiler": "^2.5.17",
"workerize-loader": "^1.0.4"
}
}

View File

@@ -1,14 +1,11 @@
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div>
<v-app id="app">
<router-view />
</v-app>
</template>
<style lang="scss">
@import "./styles/share";
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
@@ -16,14 +13,8 @@
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
html, body, #app {
@include full-page;
}
</style>

BIN
src/assets/images/bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

BIN
src/assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

15
src/i18n/en.js Normal file
View File

@@ -0,0 +1,15 @@
export default {
login: {
title: 'TIC Node Console',
inputLabel: '请输入密语例如Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eos, molestias, quisquam?',
loginBtn: 'Sign in',
errorMnemonic: 'Invalid secret word',
},
home: {
titleOwner: 'Owner',
titleChain: 'Chain',
titleNode: 'Node',
titleNetwork: 'Network',
titleMarket: 'Market',
},
}

39
src/i18n/index.js Normal file
View File

@@ -0,0 +1,39 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import { storage } from '../services'
import en from './en'
import axios from 'axios'
Vue.use(VueI18n)
const loadedLanguages = [ 'en' ]
export const i18n = new VueI18n({
locale: 'en',
fallbackLocale: 'en',
messages: { en },
})
function setI18nLanguage(lang) {
i18n.locale = lang
axios.defaults.headers.common['Accept-Language'] = lang
document.querySelector('html').setAttribute('lang', lang)
storage.currentLanguage = lang
return lang
}
export async function loadLanguageAsync(lang) {
// noinspection JSIgnoredPromiseFromCall
if (i18n.locale !== lang) {
if (!loadedLanguages.includes(lang)) {
const langRes = await import(`@/i18n/${lang}`)
loadedLanguages.push(lang)
i18n.setLocaleMessage(lang, langRes.default)
return setI18nLanguage(lang)
}
return setI18nLanguage(lang)
}
return lang
}
loadLanguageAsync(storage.currentLanguage)

15
src/i18n/zh-CN.js Normal file
View File

@@ -0,0 +1,15 @@
export default {
login: {
title: 'TIC Node 节点控制台',
inputLabel: '请输入密语例如Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eos, molestias, quisquam?',
loginBtn: '登录',
errorMnemonic: '输入的密语无效',
},
home: {
titleOwner: '个人中心',
titleChain: '区块/交易',
titleNode: '当前节点',
titleNetwork: '网络链接',
titleMarket: '应用商城',
},
}

View File

@@ -1,8 +1,10 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import { app } from './services'
import { i18n } from './i18n'
import store from './store'
import './services/ui'
Vue.config.productionTip = false
Vue.config.devtools = app.dev
@@ -11,5 +13,6 @@ Vue.config.performance = !app.dev
new Vue({
router,
store,
i18n,
render: h => h(App),
}).$mount('#app')

39
src/plugins/vuetify.js Normal file
View File

@@ -0,0 +1,39 @@
import Vue from 'vue'
import {
Vuetify,
VApp,
VNavigationDrawer,
VFooter,
VList,
VBtn,
VIcon,
VGrid,
VToolbar,
transitions
} from 'vuetify'
import 'vuetify/src/stylus/app.styl'
Vue.use(Vuetify, {
components: {
VApp,
VNavigationDrawer,
VFooter,
VList,
VBtn,
VIcon,
VGrid,
VToolbar,
transitions
},
theme: {
primary: '#ee44aa',
secondary: '#424242',
accent: '#82B1FF',
error: '#FF5252',
info: '#2196F3',
success: '#4CAF50',
warning: '#FFC107'
},
customProperties: true,
iconfont: 'md',
})

View File

@@ -1,23 +1,68 @@
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import store from './store'
import Home from './views/Home'
import Login from './views/Login'
import Owner from './views/Owner'
import Chain from './views/Chain'
import Node from './views/Node'
import Network from './views/Network'
import Market from './views/Market'
Vue.use(Router)
export default new Router({
const router = new Router({
routes: [
{
path: '/',
name: 'home',
component: Home,
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('./views/About.vue'),
children: [
{
path: 'login',
component: Login,
},
{
path: 'owner',
component: Owner,
},
{
path: 'chain',
component: Chain,
},
{
path: 'node',
component: Node,
},
{
path: 'network',
component: Network,
},
{
path: 'market',
component: Market,
},
{
path: '*',
redirect: 'owner',
},
],
},
],
})
router.beforeEach((to, _, next) => {
const loginPage = to.path === '/login'
const signed = store.getters.isLogin
if (!loginPage && !signed) {
next('/login')
return
}
if (loginPage && signed) {
next('/owner')
return
}
next()
})
export default router

View File

@@ -0,0 +1 @@
export { default as ticApi } from './tic'

View File

@@ -0,0 +1,6 @@
import { Crypto as ticCrypto } from 'tic.common'
export const secword2PubKey = secword => ticCrypto.seckey2pubkey(secword)
export const secword2Address = secword => ticCrypto.secword2address(secword)
export const isAddress = address => ticCrypto.isAddress(address)
export const secword2Account = secword => ticCrypto.secword2account(secword)

11
src/services/api/tic.js Normal file
View File

@@ -0,0 +1,11 @@
// eslint-disable-next-line
import * as worker from 'workerize-loader!./tic-bg'
const instance = worker()
export default {
secword2PubKey: async secword => instance.secword2PubKey(secword),
secword2Address: async secword => instance.secword2Address(secword),
isAddress: async address => instance.isAddress(address),
secword2Account: async secword => instance.secword2Account(secword),
}

View File

@@ -1 +1,3 @@
export { default as app } from './app'
export { default as storage } from './storage'
export * from './api'

40
src/services/storage.js Normal file
View File

@@ -0,0 +1,40 @@
const KEY_SETTING_STORAGE = 'KEY_SETTING_STORAGE'
const retrieveSettings = () => {
try {
return JSON.parse(sessionStorage.getItem(KEY_SETTING_STORAGE)) || {}
} catch (e) {
return {}
}
}
const restoreSettings =
settings => sessionStorage.setItem(KEY_SETTING_STORAGE, JSON.stringify(settings))
const supportLangs = [
{
code: 'en',
label: 'English',
},
{
code: 'zh-CN',
label: '简体中文',
},
]
export default {
get currentLanguage() {
const settings = retrieveSettings()
let { lang } = settings
if (!lang) {
settings.lang = navigator.language
lang = settings.lang
}
return lang
},
set currentLanguage(lang) {
const settings = retrieveSettings()
settings.lang = lang
restoreSettings(settings)
},
supportLangs,
}

30
src/services/ui.js Normal file
View File

@@ -0,0 +1,30 @@
import Vue from 'vue'
import { Vuetify } from 'vuetify'
import {
VApp,
VBtn,
VMenu,
VIcon,
VSelect,
VTextField,
} from 'vuetify/es5/components'
import { Ripple } from 'vuetify/es5/directives'
import 'vuetify/dist/vuetify.min.css'
import 'material-design-icons-iconfont/dist/material-design-icons.css'
Vue.use(Vuetify, {
theme: {
primary: '#4666b9',
secondary: '#dfa24e',
accent: '#FFCDD2',
},
})
Vue.component('VApp', VApp)
Vue.component('VBtn', VBtn)
Vue.component('VMenu', VMenu)
Vue.component('VIcon', VIcon)
Vue.component('VSelect', VSelect)
Vue.component('VTextField', VTextField)
Vue.directive('Ripple', Ripple)

View File

@@ -1,8 +0,0 @@
import Vuex from 'vuex'
export default new Vuex.Store({
state: {},
mutations: {},
actions: {},
getters: {},
})

View File

@@ -1,17 +1,27 @@
import Vue from 'vue'
import Vuex from 'vuex'
import chain from './chain'
import market from './market'
import network from './network'
import owner from './owner'
import * as modules from './modules'
import { app } from '../services'
import rootStore from './rootStore'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
chain,
owner,
market,
network,
},
/**
* @type {Store}
*/
const store = new Vuex.Store({
modules: { ...modules },
strict: app.dev,
...rootStore,
})
if (module.hot) {
module.hot.accept(['./modules'], () => {
const newModules = require('./modules')
store.hotUpdate({
modules: { ...newModules },
})
})
}
export default store

View File

@@ -0,0 +1,8 @@
export default {
namespaced: true,
state: {
},
mutations: {},
actions: {},
getters: {},
}

View File

@@ -0,0 +1,6 @@
export { default as chain } from './chain'
export { default as market } from './market'
export { default as network } from './network'
export { default as node } from './node'
export { default as owner } from './owner'
export { default as login } from './login'

View File

@@ -0,0 +1,50 @@
import { ticApi } from '../../services'
import router from '../../router'
import { i18n } from '../../i18n'
import _ from 'lodash'
export default {
namespaced: true,
state: {
mnemonic: '',
errorMnemonic: '',
signing: false,
},
getters: {
inputError: state => !!state.errorMnemonic,
},
mutations: {
updateMnemonic(state, mnemonic) {
if (state.errorMnemonic) {
state.errorMnemonic = ''
}
state.mnemonic = mnemonic
},
updateError(state, error) {
state.errorMnemonic = error
},
updateSign(state, signing) {
state.signing = signing
},
},
actions: {
async login({ state, commit }) {
commit('updateSign', true)
const account = (await ticApi.secword2Account(state.mnemonic)) || {}
console.log(account)
commit('updateSign', false)
if (await ticApi.isAddress(account.address)) {
commit('updateAccount', _.omit(account, 'seckey'), { root: true })
router.push('/owner')
} else {
commit('updateError', i18n.t('login.errorMnemonic'))
}
},
},
}

View File

@@ -1,8 +1,7 @@
import Vuex from 'vuex'
export default new Vuex.Store({
export default {
namespaced: true,
state: {},
mutations: {},
actions: {},
getters: {},
})
}

View File

@@ -1,8 +1,7 @@
import Vuex from 'vuex'
export default new Vuex.Store({
export default {
namespaced: true,
state: {},
mutations: {},
actions: {},
getters: {},
})
}

View File

@@ -1,8 +1,7 @@
import Vuex from 'vuex'
export default new Vuex.Store({
export default {
namespaced: true,
state: {},
mutations: {},
actions: {},
getters: {},
})
}

View File

@@ -1,8 +1,7 @@
import Vuex from 'vuex'
export default new Vuex.Store({
export default {
namespaced: true,
state: {},
mutations: {},
actions: {},
getters: {},
})
}

96
src/store/rootStore.js Normal file
View File

@@ -0,0 +1,96 @@
import { storage } from '../services'
import { loadLanguageAsync } from '../i18n'
import router from '../router'
const KEY_ACCOUNT = 'KEY_ACCOUNT'
function retrieveAccount() {
try {
return JSON.parse(sessionStorage.getItem(KEY_ACCOUNT))
} catch (e) {
return {}
}
}
function storeAccount(account) {
sessionStorage.setItem(KEY_ACCOUNT, JSON.stringify(account))
}
function clearAccount() {
sessionStorage.removeItem(KEY_ACCOUNT)
}
const sideMenus = [
{
title: 'home.titleOwner',
icon: 'group',
path: '/owner',
},
{
title: 'home.titleChain',
icon: 'public',
path: '/chain',
},
{
title: 'home.titleNode',
icon: 'all_inclusive',
path: '/node',
},
{
title: 'home.titleNetwork',
icon: 'language',
path: '/network',
},
{
title: 'home.titleMarket',
icon: 'apps',
path: '/market',
},
]
export default {
state: {
account: retrieveAccount() || {},
supportLangs: storage.supportLangs,
currentLang: storage.currentLanguage,
sideMenus,
sideCollapsed: false,
},
getters: {
isLogin: state => state.account.pubkey,
currentPath: () => router.currentRoute.path,
},
mutations: {
updateCurrentLang(state, lang) {
state.currentLang = lang
},
updateAccount(state, account) {
state.account = account || {}
storeAccount(state.account)
},
updateRoute(_, destination) {
router.push(destination)
},
toggleCollapse(state) {
state.sideCollapsed = !state.sideCollapsed
},
logout(state) {
state.account = {}
clearAccount()
router.push('/login')
},
},
actions: {
async updateCurrentLang({ commit }, lang) {
commit('updateCurrentLang', await loadLanguageAsync(lang))
},
},
}

12
src/styles/_share.scss Normal file
View File

@@ -0,0 +1,12 @@
$primaryColor: #4666b9;
$secondaryColor: #dfa24e;
$accentColor: #FFCDD2;
@mixin full-page {
width: 100%;
height: 100%;
}
.v-icon {
user-select: none;
}

View File

@@ -1,5 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

16
src/views/Chain.vue Normal file
View File

@@ -0,0 +1,16 @@
<template>
<div class="chain-page">
Chain
</div>
</template>
<script>
export default {
name: 'Chain',
}
</script>
<style scoped lang="scss">
.chain-page {
}
</style>

View File

@@ -1,11 +1,255 @@
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<div class="side-menus" :class="{ collapsed: sideCollapsed || !isLogin }">
<div class="side-title">
<img src="../assets/images/logo.png" />
<span class="white--text">TIME IN CHAIN</span>
</div>
<div
class="side-menu-item"
v-for="menu in sideMenus"
@click="updateRoute(menu.path)"
:class="{ active: sideSelected === menu.path }"
:key="menu.path">
<v-icon dark x-large>{{ menu.icon }}</v-icon>
<div class="title white--text">{{ $t(menu.title) }}</div>
</div>
</div>
<div class="app-body">
<div class="app-header" :class="{ collapsed: !isLogin }">
<div class="indicator" @click="toggleCollapse" :class="{ collapsed: sideCollapsed }">
<v-icon class="right">chevron_right</v-icon>
<v-icon class="left">chevron_left</v-icon>
</div>
<div class="title primary--text">{{ $t('login.title') }}</div>
<v-select
color="secondary"
prepend-icon="language"
:value="currentLang"
@input="updateCurrentLang"
:items="supportLangs"
item-value="code"
item-text="label"
/>
<div class="separator"></div>
<v-icon @click="logout">power_settings_new</v-icon>
</div>
<router-view class="app-container" />
</div>
</div>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
name: 'Home',
data() {
return {
sideSelected: this.$route.path,
}
},
computed: {
...mapState([
'sideMenus',
'sideCollapsed',
'supportLangs',
'currentLang',
]),
...mapGetters([
'isLogin',
]),
},
methods: {
...mapMutations([
'updateRoute',
'toggleCollapse',
'logout',
]),
...mapActions([
'updateCurrentLang',
]),
},
watch: {
'$route.path'(to) {
this.sideSelected = to
},
},
}
</script>
<style lang="scss" scoped>
@import "../styles/share";
.home {
&, .app-body {
@include full-page;
}
display: flex;
$sideWidth: 260px;
$headerHeight: 64px;
.side-menus {
&.collapsed {
margin-left: -$sideWidth;
}
width: $sideWidth;
background: url('../assets/images/bg.jpg') center;
background-size: cover;
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
align-items: stretch;
box-shadow: 1px 0 2px #ddd;
transition: margin-left .4s;
.side-title {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 0;
& > img {
width: 80px;
}
}
.side-menu-item {
cursor: pointer;
padding: 40px 0;
transition: background-color .4s;
position: relative;
overflow: hidden;
.v-icon {
font-size: 48px!important;
}
.title {
margin: 4px 0;
font-size: 1.2em!important;
}
&:hover {
background-color: rgba($secondaryColor, .15);
}
$indicatorSize: 24px;
&:before {
content: '';
display: block;
width: $indicatorSize;
height: $indicatorSize;
background-color: #fff;
position: absolute;
right: -$indicatorSize*2;
top: 50%;
transform: translateY(-50%) rotate(45deg);
transition: right .4s;
}
&.active {
background-color: rgba(white, .25);
&:before {
right: -$indicatorSize/2;
}
}
}
}
.app-body {
display: flex;
flex-direction: column;
.app-header {
&.collapsed {
margin-top: -$headerHeight;
}
transition: margin-top .4s;
flex-shrink: 0;
height: $headerHeight;
background-color: white;
box-shadow: 0 1px 2px #ddd;
display: flex;
align-items: center;
.indicator {
position: relative;
$iconSize: 28px;
width: $iconSize;
align-self: stretch;
margin: 0 40px;
cursor: pointer;
.v-icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
font-size: $iconSize!important;
}
.left {
left: 45%;
}
.right {
left: -15%;
}
&.collapsed {
.left {
left: -15%;
}
.right {
left: 45%;
}
}
}
.title {
flex-grow: 1;
text-align: left;
}
.v-select {
width: 160px;
flex-grow: 0;
flex-shrink: 0;
}
.separator {
align-self: stretch;
margin: 20px;
width: 1px;
background-color: gray;
}
& > .v-icon {
margin: 0 40px 0 0;
cursor: pointer;
}
}
.app-container {
flex-grow: 1;
}
}
}
</style>

106
src/views/Login.vue Normal file
View File

@@ -0,0 +1,106 @@
<template>
<div class="login-page">
<img src="../assets/images/logo.png">
<h1 class="white--text">{{ $t('login.title') }}</h1>
<v-text-field
color="secondary"
dark autofocus
single-line
:error="inputError"
:error-messages="errorMnemonic"
:value="mnemonic"
:loading="signing"
@input="updateMnemonic"
:label="$t('login.inputLabel')" />
<v-select
color="secondary" dark
prepend-icon="language"
:value="currentLang"
@input="updateCurrentLang"
:items="supportLangs"
item-value="code"
item-text="label"
/>
<v-btn :loading="signing" dark large color="transparent" @click="login">
{{ $t('login.loginBtn') }}
</v-btn>
</div>
</template>
<script>
import {
createNamespacedHelpers,
mapState as mapRootState,
mapActions as mapRootActions,
} from 'vuex'
const { mapState, mapGetters, mapMutations, mapActions } = createNamespacedHelpers('login')
export default {
name: 'Login',
computed: {
...mapState([
'mnemonic',
'errorMnemonic',
'signing',
]),
...mapGetters([
'inputError',
]),
...mapRootState([
'supportLangs',
'currentLang',
]),
},
methods: {
...mapMutations([
'updateMnemonic',
]),
...mapActions([
'login',
]),
...mapRootActions([
'updateCurrentLang',
]),
},
}
</script>
<style scoped lang="scss">
.login-page {
background: url('../assets/images/bg.jpg') center;
background-size: cover;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 24%;
& > img {
width: 120px;
margin-top: -10%;
}
& > h1 {
margin: 40px 0;
}
& /deep/ .v-text-field {
align-self: stretch;
flex-grow: 0;
margin: 10px 0;
}
& /deep/ .v-btn {
align-self: stretch;
box-shadow: 0 4px 14px #111 !important;
}
}
</style>

16
src/views/Market.vue Normal file
View File

@@ -0,0 +1,16 @@
<template>
<div class="market-page">
Market
</div>
</template>
<script>
export default {
name: 'Market',
}
</script>
<style scoped lang="scss">
.market-page {
}
</style>

16
src/views/Network.vue Normal file
View File

@@ -0,0 +1,16 @@
<template>
<div class="network-page">
Network
</div>
</template>
<script>
export default {
name: 'Network',
}
</script>
<style scoped lang="scss">
.network-page {
}
</style>

16
src/views/Node.vue Normal file
View File

@@ -0,0 +1,16 @@
<template>
<div class="node-page">
Node
</div>
</template>
<script>
export default {
name: 'Node',
}
</script>
<style scoped lang="scss">
.node-page {
}
</style>

16
src/views/Owner.vue Normal file
View File

@@ -0,0 +1,16 @@
<template>
<div class="owner-page">
Owner
</div>
</template>
<script>
export default {
name: 'Owner',
}
</script>
<style scoped lang="scss">
.owner-page {
}
</style>