diff --git a/.browserslistrc b/.browserslistrc index 9dee646..8f96043 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,3 +1,3 @@ > 1% last 2 versions -not ie <= 8 +not ie <= 10 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..3dddf3f --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +dist/* +node_modules/* diff --git a/.eslintrc.js b/.eslintrc.js index a28165f..e3430cf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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 diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..08844f3 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +registry=http://101.132.121.111:6842 diff --git a/babel.config.js b/babel.config.js index d39e57b..e113d3b 100644 --- a/babel.config.js +++ b/babel.config.js @@ -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, + }, + }, + ], ], } diff --git a/package.json b/package.json index b68dc33..f7c0b3e 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/App.vue b/src/App.vue index f300d4b..352dcce 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,14 +1,11 @@ diff --git a/src/assets/images/bg.jpg b/src/assets/images/bg.jpg new file mode 100644 index 0000000..fd803a4 Binary files /dev/null and b/src/assets/images/bg.jpg differ diff --git a/src/assets/images/logo.png b/src/assets/images/logo.png new file mode 100644 index 0000000..64cdb12 Binary files /dev/null and b/src/assets/images/logo.png differ diff --git a/src/i18n/en.js b/src/i18n/en.js new file mode 100644 index 0000000..c64f402 --- /dev/null +++ b/src/i18n/en.js @@ -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', + }, +} diff --git a/src/i18n/index.js b/src/i18n/index.js new file mode 100644 index 0000000..06c43a6 --- /dev/null +++ b/src/i18n/index.js @@ -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) diff --git a/src/i18n/zh-CN.js b/src/i18n/zh-CN.js new file mode 100644 index 0000000..dddb836 --- /dev/null +++ b/src/i18n/zh-CN.js @@ -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: '应用商城', + }, +} diff --git a/src/main.js b/src/main.js index a9ef81c..1a5358d 100644 --- a/src/main.js +++ b/src/main.js @@ -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') diff --git a/src/plugins/vuetify.js b/src/plugins/vuetify.js new file mode 100644 index 0000000..da656fd --- /dev/null +++ b/src/plugins/vuetify.js @@ -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', +}) diff --git a/src/router.js b/src/router.js index 5cacada..55408ac 100644 --- a/src/router.js +++ b/src/router.js @@ -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 diff --git a/src/services/api/index.js b/src/services/api/index.js new file mode 100644 index 0000000..b507f38 --- /dev/null +++ b/src/services/api/index.js @@ -0,0 +1 @@ +export { default as ticApi } from './tic' diff --git a/src/services/api/tic-bg.js b/src/services/api/tic-bg.js new file mode 100644 index 0000000..7a2f1be --- /dev/null +++ b/src/services/api/tic-bg.js @@ -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) diff --git a/src/services/api/tic.js b/src/services/api/tic.js new file mode 100644 index 0000000..bda6429 --- /dev/null +++ b/src/services/api/tic.js @@ -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), +} diff --git a/src/services/index.js b/src/services/index.js index 694eac2..d0ebc02 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -1 +1,3 @@ export { default as app } from './app' +export { default as storage } from './storage' +export * from './api' diff --git a/src/services/storage.js b/src/services/storage.js new file mode 100644 index 0000000..119bf50 --- /dev/null +++ b/src/services/storage.js @@ -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, +} diff --git a/src/services/ui.js b/src/services/ui.js new file mode 100644 index 0000000..903269e --- /dev/null +++ b/src/services/ui.js @@ -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) diff --git a/src/store/chain.js b/src/store/chain.js deleted file mode 100644 index 56e2b7b..0000000 --- a/src/store/chain.js +++ /dev/null @@ -1,8 +0,0 @@ -import Vuex from 'vuex' - -export default new Vuex.Store({ - state: {}, - mutations: {}, - actions: {}, - getters: {}, -}) diff --git a/src/store/index.js b/src/store/index.js index a3e8d37..2b3af08 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -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 diff --git a/src/store/modules/chain.js b/src/store/modules/chain.js new file mode 100644 index 0000000..623ab4b --- /dev/null +++ b/src/store/modules/chain.js @@ -0,0 +1,8 @@ +export default { + namespaced: true, + state: { + }, + mutations: {}, + actions: {}, + getters: {}, +} diff --git a/src/store/modules/index.js b/src/store/modules/index.js new file mode 100644 index 0000000..d387235 --- /dev/null +++ b/src/store/modules/index.js @@ -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' diff --git a/src/store/modules/login.js b/src/store/modules/login.js new file mode 100644 index 0000000..28e0be6 --- /dev/null +++ b/src/store/modules/login.js @@ -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')) + } + }, + }, +} diff --git a/src/store/market.js b/src/store/modules/market.js similarity index 50% rename from src/store/market.js rename to src/store/modules/market.js index 56e2b7b..c3240d4 100644 --- a/src/store/market.js +++ b/src/store/modules/market.js @@ -1,8 +1,7 @@ -import Vuex from 'vuex' - -export default new Vuex.Store({ +export default { + namespaced: true, state: {}, mutations: {}, actions: {}, getters: {}, -}) +} diff --git a/src/store/network.js b/src/store/modules/network.js similarity index 50% rename from src/store/network.js rename to src/store/modules/network.js index 56e2b7b..c3240d4 100644 --- a/src/store/network.js +++ b/src/store/modules/network.js @@ -1,8 +1,7 @@ -import Vuex from 'vuex' - -export default new Vuex.Store({ +export default { + namespaced: true, state: {}, mutations: {}, actions: {}, getters: {}, -}) +} diff --git a/src/store/node.js b/src/store/modules/node.js similarity index 50% rename from src/store/node.js rename to src/store/modules/node.js index 56e2b7b..c3240d4 100644 --- a/src/store/node.js +++ b/src/store/modules/node.js @@ -1,8 +1,7 @@ -import Vuex from 'vuex' - -export default new Vuex.Store({ +export default { + namespaced: true, state: {}, mutations: {}, actions: {}, getters: {}, -}) +} diff --git a/src/store/owner.js b/src/store/modules/owner.js similarity index 50% rename from src/store/owner.js rename to src/store/modules/owner.js index 56e2b7b..c3240d4 100644 --- a/src/store/owner.js +++ b/src/store/modules/owner.js @@ -1,8 +1,7 @@ -import Vuex from 'vuex' - -export default new Vuex.Store({ +export default { + namespaced: true, state: {}, mutations: {}, actions: {}, getters: {}, -}) +} diff --git a/src/store/rootStore.js b/src/store/rootStore.js new file mode 100644 index 0000000..afb045e --- /dev/null +++ b/src/store/rootStore.js @@ -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)) + }, + }, +} diff --git a/src/styles/_share.scss b/src/styles/_share.scss new file mode 100644 index 0000000..f804a1d --- /dev/null +++ b/src/styles/_share.scss @@ -0,0 +1,12 @@ +$primaryColor: #4666b9; +$secondaryColor: #dfa24e; +$accentColor: #FFCDD2; + +@mixin full-page { + width: 100%; + height: 100%; +} + +.v-icon { + user-select: none; +} diff --git a/src/views/About.vue b/src/views/About.vue deleted file mode 100644 index 3fa2807..0000000 --- a/src/views/About.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/src/views/Chain.vue b/src/views/Chain.vue new file mode 100644 index 0000000..0c898d7 --- /dev/null +++ b/src/views/Chain.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/src/views/Home.vue b/src/views/Home.vue index 0e5cba5..19e8e69 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -1,11 +1,255 @@ + + diff --git a/src/views/Login.vue b/src/views/Login.vue new file mode 100644 index 0000000..413cff1 --- /dev/null +++ b/src/views/Login.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/src/views/Market.vue b/src/views/Market.vue new file mode 100644 index 0000000..22fdd47 --- /dev/null +++ b/src/views/Market.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/src/views/Network.vue b/src/views/Network.vue new file mode 100644 index 0000000..2270591 --- /dev/null +++ b/src/views/Network.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/src/views/Node.vue b/src/views/Node.vue new file mode 100644 index 0000000..abf2a8d --- /dev/null +++ b/src/views/Node.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/src/views/Owner.vue b/src/views/Owner.vue new file mode 100644 index 0000000..adc2bba --- /dev/null +++ b/src/views/Owner.vue @@ -0,0 +1,16 @@ + + + + +