├── .env.development ├── .env.production ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── ci_check.yml ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Changelog.md ├── LICENSE ├── README.md ├── babel.config.js ├── build └── index.js ├── docs └── images │ └── menu_logo_wecross.png ├── jest.config.js ├── mock ├── conn.js ├── index.js ├── mock-server.js ├── resource.js ├── status.js ├── transaction.js ├── ua.js ├── user.js └── utils.js ├── package.json ├── plop-templates ├── component │ ├── index.hbs │ └── prompt.js ├── store │ ├── index.hbs │ └── prompt.js ├── utils.js └── view │ ├── index.hbs │ └── prompt.js ├── plopfile.js ├── public ├── favicon.ico └── index.html ├── release_note.txt ├── src ├── .travis.yml ├── App.vue ├── api │ ├── common.js │ ├── conn.js │ ├── resource.js │ ├── status.js │ ├── transaction.js │ ├── ua.js │ └── user.js ├── assets │ ├── 404_images │ │ ├── 404.png │ │ └── 404_cloud.png │ ├── QRCode.jpg │ ├── app.png │ ├── chain.png │ ├── check-fail.svg │ ├── check-pass.svg │ ├── favicon.svg │ ├── icon-bc.svg │ ├── icon-ly.svg │ ├── icon-sw.svg │ ├── icon-zy.svg │ ├── issue.png │ ├── nav-logo.svg │ ├── router.png │ └── wecross.svg ├── components │ ├── Breadcrumb │ │ └── index.vue │ ├── ChainExplorer │ │ └── index.vue │ ├── Clipboard │ │ └── index.vue │ ├── Hamburger │ │ └── index.vue │ ├── HeaderSearch │ │ └── index.vue │ ├── ResourceExplorer │ │ └── index.vue │ ├── ResourceShower │ │ └── index.vue │ ├── ResourceTransfer │ │ └── index.vue │ ├── SvgIcon │ │ └── index.vue │ └── TransactionListExplorer │ │ └── index.vue ├── icons │ ├── index.js │ ├── svg │ │ ├── chicken.svg │ │ ├── exit-fullscreen.svg │ │ ├── eye-open.svg │ │ ├── eye.svg │ │ ├── fullscreen.svg │ │ ├── password.svg │ │ ├── qrcode.svg │ │ ├── question.svg │ │ ├── search.svg │ │ ├── table.svg │ │ ├── user-avatar.svg │ │ └── user.svg │ └── svgo.yml ├── layout │ ├── components │ │ ├── AppMain.vue │ │ ├── Navbar.vue │ │ ├── Sidebar │ │ │ ├── FixiOSBug.js │ │ │ ├── Item.vue │ │ │ ├── Link.vue │ │ │ ├── Logo.vue │ │ │ ├── SidebarItem.vue │ │ │ └── index.vue │ │ └── index.js │ ├── index.vue │ └── mixin │ │ └── ResizeHandler.js ├── main.js ├── permission.js ├── router │ └── index.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── app.js │ │ ├── permission.js │ │ ├── transaction.js │ │ └── user.js ├── styles │ ├── element-ui.scss │ ├── element-variables.scss │ ├── index.scss │ ├── intro.scss │ ├── mixin.scss │ ├── sidebar.scss │ ├── transition.scss │ └── variables.scss ├── utils │ ├── auth.js │ ├── authcode.js │ ├── chainAccountIntro.js │ ├── clipboard.js │ ├── errorcode.js │ ├── get-page-title.js │ ├── index.js │ ├── messageBox.js │ ├── pem.js │ ├── request.js │ ├── resource.js │ ├── rsa.js │ ├── sm3.js │ ├── transaction.js │ └── validate.js └── views │ ├── 404.vue │ ├── access │ └── index.vue │ ├── account │ ├── changePassword.vue │ └── index.vue │ ├── document │ └── index.vue │ ├── homepage │ └── index.vue │ ├── login │ └── index.vue │ ├── register │ └── index.vue │ ├── resource │ ├── resourceDeployment.vue │ ├── resourceManager.vue │ └── resourceSteps │ │ ├── resourceDeploySteps.js │ │ └── resourceManagerSteps.js │ ├── router │ ├── routerGuide.vue │ └── routerManager.vue │ └── transaction │ ├── components │ └── TransactionForm.vue │ ├── rawTransaction.vue │ ├── transactionManager.vue │ ├── transactionSteps │ ├── transactionManagerSteps.js │ └── xaTransactionStep.js │ ├── xaTransaction.vue │ └── xaTransactionList.vue ├── tests └── unit │ ├── .eslintrc.js │ └── utils │ ├── formatTime.spec.js │ ├── param2Obj.spec.js │ ├── parseTime.spec.js │ └── validate.spec.js └── vue.config.js /.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '' 6 | VUE_APP_MOCK_API = '/dev-api' -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/' 6 | 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /.github/workflows/ci_check.yml: -------------------------------------------------------------------------------- 1 | name: CI Check 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | check: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2-beta 11 | with: 12 | node-version: '10' 13 | - name: npm install 14 | run: npm install 15 | - name: Build production 16 | run: npm install && npm run build:prod 17 | - name: Test 18 | run: npm run test:ci 19 | 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | package-lock.json 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # safelist 2 | branches: 3 | only: 4 | - /.*/ 5 | 6 | matrix: 7 | fast_finish: true 8 | include: 9 | 10 | - language: node_js 11 | node_js: 10 12 | script: npm install && npm run build:prod && npm run test:ci 13 | os: linux 14 | dist: xenial 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献代码 2 | 3 | 非常感谢能有心为WeCross贡献代码! 4 | 5 | ## 分支策略 6 | 7 | 项目采用[git-flow](https://jeffkreeftmeijer.com/git-flow/)的分支策略。 8 | 9 | * master:最新的稳定分支 10 | * dev:待发布的稳定分支 11 | * feature-xxxx:一个正在开发xxxx特性分支 12 | * bugfix-xxxx:一个正在修bug xxxx的分支 13 | 14 | ## 贡献方法 15 | 16 | ### Issue 17 | 18 | 可直接去[issues page](https://github.com/WeBankBlockchain/WeCross-WebApp/issues)提issue。 19 | 20 | ### 修复bug 21 | 22 | 1. Fork本仓库到个人仓库 23 | 2. 从个人仓库的master分支拉出一个bugfix-xxxx分支 24 | 3. 在bugfix-xxxx上修复bug 25 | 4. 测试修复的bug 26 | 5. PR(Pull Request)到本仓库的dev分支 27 | 6. 等待社区review这个PR 28 | 7. PR合入,bug修复完成! 29 | 30 | ### 开发新特性 31 | 32 | 1. Fork本仓库到个人仓库 33 | 2. 从个人仓库的dev分支拉出一个feature-xxxx分支 34 | 3. 在feature-xxxx上进行特性开发 35 | 4. 不定期的从本仓库的dev分支pull最新的改动到feature-xxxx分支 36 | 5. 测试新特性 37 | 6. PR(Pull Request)到本参考的dev分支 38 | 7. 等待社区review这个PR 39 | 8. PR合入,特性开发完成! 40 | 41 | ## 代码格式化 42 | 43 | 代码格式化gradle插件[google-java-format-gradle-plugin](https://github.com/sherter/google-java-format-gradle-plugin). 44 | 45 | 执行任务 `googleJavaFormat`格式化java文件。 46 | ``` 47 | ./gradlew goJF 48 | ``` 49 | 执行任务 `verifyGoogleJavaFormat`验证java文件是否格式化完成 50 | ``` 51 | ./gradlew verGJF 52 | ``` 53 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | ### v1.4.0 2 | 3 | (2024-03-01) 4 | 5 | **新增** 6 | 7 | - 适配 `systemStatus` 的GET接口,获取系统信息在首页展示 https://github.com/WeBankBlockchain/WeCross-WebApp/pull/180 8 | - 事务列表页面新增按链查询的功能,优化事务查询效率 https://github.com/WeBankBlockchain/WeCross-WebApp/pull/182 9 | 10 | ### v1.3.1 11 | 12 | (2023-07-31) 13 | 14 | **新增** 15 | 16 | * 新增对FISCO BCOS 3.x版本WASM合约的部署调用 17 | 18 | ### v1.3.0 19 | 20 | (2023-03-15) 21 | 22 | **新增** 23 | 24 | * 新增对FISCO BCOS 3.x版本的支持 25 | * 新增Email登陆注册检查的功能 26 | 27 | ### v1.2.1 28 | 29 | (2021-12-15) 30 | 31 | (无改动,配合其他组件同步更新版本) 32 | 33 | ### v1.2.0 34 | 35 | (2021-08-20) 36 | 37 | **新增** 38 | 39 | * 资源访问控制功能,管理员可通过网页管理台给用户授权可访问的资源 40 | 41 | ### v1.1.1 42 | 43 | (2021-04-02) 44 | 45 | **更改** 46 | 47 | * 增加内容复制按钮 48 | * 用户操作优化 49 | * 展示效果优化 50 | 51 | ### v1.1.0 52 | 53 | (2020-02-02) 54 | 55 | **新增** 56 | 57 | * 添加修改密码功能 58 | * 添加页面帮助指引 59 | 60 | **更新** 61 | 62 | * 体验优化 63 | * 问题修复 64 | 65 | ### v1.0.0 66 | 67 | (2020-12-17) 68 | 69 | **功能** 70 | 71 | * WeCross管理台:提供可视化的跨链管理服务 72 | 73 | **新增** 74 | 75 | * 注册与登录页:注册跨链账户,登录管理平台 76 | * 平台首页:展示跨链网络信息以及系统配置信息 77 | * 账户管理页:跨链账户生命周期管理 78 | * 路由管理页:互联的跨链路由管理 79 | * 资源管理页:跨链资源展示以及资源调用 80 | * 交易管理页:跨链交易列表以及交易详情展示 81 | * 事务管理页:事务列表展示以及事务操作 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 | # WeCross-WebApp 4 | 5 | [](https://github.com/WeBankBlockchain/WeCross-WebApp/releases/latest) [](https://www.apache.org/licenses/LICENSE-2.0) [](https://vuejs.org/index.html) 6 | 7 | WeCross WebApp是[WeCross](https://github.com/WeBankBlockchain/WeCross)可视化跨链管理平台。 8 | 9 | ## 关键特性 10 | 11 | - 跨链路由、跨链账户、跨链资源、跨链交易以及跨链事务的可视化管理 12 | - 跨链网络信息以及跨链路由系统信息的获取和展示 13 | 14 | ## 部署与使用 15 | 16 | WeCross跨链管理平台的部署和使用可参考[使用文档](https://wecross.readthedocs.io/zh_CN/latest/docs/manual/webApp.html)。 17 | 18 | ## 源码编译 19 | 20 | **环境要求**: 21 | 22 | - [node.js](https://nodejs.org/en/) 8.10及以上 23 | 24 | **编译命令**: 25 | 26 | ```shell 27 | # 初始化 28 | npm install 29 | 30 | # 编译及热重载 31 | npm run dev 32 | 33 | # 编译并最小化生产 34 | npm run build:prod 35 | 36 | # 使用lint修复代码 37 | npm run lint 38 | ``` 39 | 40 | ## 项目贡献 41 | 42 | 欢迎参与WeCross社区的维护和建设: 43 | 44 | - 提交代码(Pull requests),可参考[代码贡献流程](CONTRIBUTING.md)以及[wiki指南](https://github.com/WeBankBlockchain/WeCross/wiki/%E8%B4%A1%E7%8C%AE%E4%BB%A3%E7%A0%81) 45 | - [提问和提交BUG](https://github.com/WeBankBlockchain/WeCross/issues/new) 46 | 47 | 您将成为贡献者,感谢各位贡献者的付出: 48 | 49 | 50 | 51 | ## 开源社区 52 | 53 | 参与meetup:[活动日历](https://github.com/WeBankBlockchain/WeCross/wiki#%E6%B4%BB%E5%8A%A8%E6%97%A5%E5%8E%86) 54 | 55 | 学习知识、讨论方案、开发新特性:[联系微信小助手,加入跨链兴趣小组(CC-SIG)](https://wecross.readthedocs.io/zh_CN/latest/docs/community/cc-sig.html#id3) 56 | 57 | ## License 58 | 59 | WeCross WebApp的开源协议为Apache License 2.0,详情参考[LICENSE](./LICENSE)。 60 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | const { run } = require('runjs') 2 | const chalk = require('chalk') 3 | const config = require('../vue.config.js') 4 | const rawArgv = process.argv.slice(2) 5 | const args = rawArgv.join(' ') 6 | 7 | if (process.env.npm_config_preview || rawArgv.includes('--preview')) { 8 | const report = rawArgv.includes('--report') 9 | 10 | run(`vue-cli-service build ${args}`) 11 | 12 | const port = 9526 13 | const publicPath = config.publicPath 14 | 15 | var connect = require('connect') 16 | var serveStatic = require('serve-static') 17 | const app = connect() 18 | 19 | app.use( 20 | publicPath, 21 | serveStatic('./dist', { 22 | index: ['index.html', '/'] 23 | }) 24 | ) 25 | 26 | app.listen(port, function () { 27 | console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) 28 | if (report) { 29 | console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) 30 | } 31 | 32 | }) 33 | } else { 34 | run(`vue-cli-service build ${args}`) 35 | } 36 | -------------------------------------------------------------------------------- /docs/images/menu_logo_wecross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankBlockchain/WeCross-WebApp/ff0a2850a465d5e7167cdb70a03d45d19cbb4963/docs/images/menu_logo_wecross.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/unit/**/*.{js,vue}', '!src/unit/auth.js', '!src/unit/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /mock/conn.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | module.exports = [{ 3 | url: '/conn/listPeers', 4 | type: 'get', 5 | response: _ => { 6 | return { 7 | ...Mock.mock({ 8 | version: '1', 9 | errorCode: 0, 10 | message: 'success', 11 | data: { 12 | size: 1000, 13 | 'data|10': [{ 14 | nodeID: '@id', 15 | address: '@integer(1,255).@integer(1,255).@integer(1,255).@integer(1,255):@integer(1, 65535)', 16 | seq: 1, 17 | chainInfos: [{ 18 | name: '@id', 19 | stubType: '@pick([\'Fabirc1.4\', \'BCOS2.0\', \'GM_BCOS2.0\'])' 20 | }] 21 | }] 22 | } 23 | }) } 24 | } 25 | }, 26 | { 27 | url: '/conn/addPeer', 28 | type: 'post', 29 | response: config => { 30 | /* 31 | peerData.items.push({ 32 | nodeID: '@id', 33 | address: config.body.data.address, 34 | seq: 1, 35 | chainInfos: [{ 36 | name: 'bcos', 37 | stubType: 'BCOS2.0' 38 | }] 39 | }) 40 | */ 41 | 42 | return { 43 | 'version': '1', 44 | 'errorCode': 0, 45 | 'message': 'success', 46 | 'peerData': { 47 | 'errorCode': 0, 48 | 'message': 'success' 49 | } 50 | } 51 | } 52 | }, { 53 | url: '/conn/removePeer', 54 | type: 'post', 55 | response: _ => { 56 | // for (var i in peerData.items) { 57 | // if (peerData.items[i].address === config.body.data.address) { 58 | // peerData.items.splice(i, 1) 59 | // break 60 | // } 61 | // } 62 | return { 63 | 'version': '1', 64 | 'errorCode': 0, 65 | 'message': 'success', 66 | 'peerData': { 67 | 'errorCode': 0, 68 | 'message': 'success' 69 | } 70 | } 71 | } 72 | }, { 73 | url: '/conn/listChains', 74 | type: 'get', 75 | response: param => { 76 | return { 77 | 'version': '1', 78 | 'errorCode': 0, 79 | 'message': 'success', 80 | data: { 81 | size: 1000, 82 | 'data|10': [{ 83 | zone: param.query.zone, 84 | 'chain|1': ['bcos@integer(1,10000)', 'bcos_gm@integer(1,10000)', 'fabric@integer(1,10000)'], 85 | 'type|1': ['BCOS2.0', 'GM_BCOS2.0', 'Fabric1.4'], 86 | blockNumber: '@integer(1,1000000)', 87 | isLocal: '@pick(true,false)', 88 | 'properties': { 89 | 'BCOS_PROPERTY_CHAIN_ID': '1', 90 | 'WeCrossProxyABI': 'xxxxxxxxx', 91 | 'BCOS_PROPERTY_GROUP_ID': '1', 92 | 'WeCrossProxy': '0x8f9a2f54ca70f6a3f50b1ed27bdccad363b126f0', 93 | 'BCOS_PROPERTY_STUB_TYPE': 'BCOS2.0', 94 | 'WeCrossHub': '0x894b85761beec3aa08b00b9012c4ccd45c43ed84' 95 | } 96 | }] 97 | } 98 | } 99 | } 100 | }, { 101 | url: '/conn/listZones', 102 | type: 'get', 103 | response: _ => { 104 | return { 105 | 'version': '1', 106 | 'errorCode': 0, 107 | 'message': 'success', 108 | data: { 109 | size: 1, 110 | 'data|10': ["@pick([\'payment\',\'load\',\'resource\'])"] 111 | } 112 | } 113 | } 114 | } 115 | ] 116 | -------------------------------------------------------------------------------- /mock/index.js: -------------------------------------------------------------------------------- 1 | const user = require('./user') 2 | const resource = require('./resource') 3 | const conn = require('./conn') 4 | const ua = require('./ua') 5 | const transaction = require('./transaction') 6 | const status = require('./status') 7 | 8 | const mocks = [ 9 | ...user, 10 | ...resource, 11 | ...conn, 12 | ...ua, 13 | ...transaction, 14 | ...status 15 | ] 16 | 17 | module.exports = { 18 | mocks 19 | } 20 | 21 | -------------------------------------------------------------------------------- /mock/mock-server.js: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar') 2 | const bodyParser = require('body-parser') 3 | const chalk = require('chalk') 4 | const path = require('path') 5 | const Mock = require('mockjs') 6 | 7 | const mockDir = path.join(process.cwd(), 'mock') 8 | 9 | function registerRoutes(app) { 10 | let mockLastIndex 11 | const { mocks } = require('./index.js') 12 | const mocksForServer = mocks.map(route => { 13 | return responseFake(route.url, route.type, route.response) 14 | }) 15 | for (const mock of mocksForServer) { 16 | app[mock.type](mock.url, mock.response) 17 | mockLastIndex = app._router.stack.length 18 | } 19 | const mockRoutesLength = Object.keys(mocksForServer).length 20 | return { 21 | mockRoutesLength: mockRoutesLength, 22 | mockStartIndex: mockLastIndex - mockRoutesLength 23 | } 24 | } 25 | 26 | function unregisterRoutes() { 27 | Object.keys(require.cache).forEach(i => { 28 | if (i.includes(mockDir)) { 29 | delete require.cache[require.resolve(i)] 30 | } 31 | }) 32 | } 33 | 34 | // for mock server 35 | const responseFake = (url, type, respond) => { 36 | return { 37 | url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`), 38 | type: type || 'get', 39 | response(req, res) { 40 | console.log('request invoke:' + req.path + ' query:' + JSON.stringify(req.query)) 41 | res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) 42 | } 43 | } 44 | } 45 | 46 | module.exports = app => { 47 | // parse app.body 48 | // https://expressjs.com/en/4x/api.html#req.body 49 | app.use(bodyParser.json()) 50 | app.use(bodyParser.urlencoded({ 51 | extended: true 52 | })) 53 | 54 | const mockRoutes = registerRoutes(app) 55 | var mockRoutesLength = mockRoutes.mockRoutesLength 56 | var mockStartIndex = mockRoutes.mockStartIndex 57 | 58 | // watch files, hot reload mock server 59 | chokidar.watch(mockDir, { 60 | ignored: /mock-server/, 61 | ignoreInitial: true 62 | }).on('all', (event, path) => { 63 | if (event === 'change' || event === 'add') { 64 | try { 65 | // remove mock routes stack 66 | app._router.stack.splice(mockStartIndex, mockRoutesLength) 67 | 68 | // clear routes cache 69 | unregisterRoutes() 70 | 71 | const mockRoutes = registerRoutes(app) 72 | mockRoutesLength = mockRoutes.mockRoutesLength 73 | mockStartIndex = mockRoutes.mockStartIndex 74 | 75 | console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`)) 76 | } catch (error) { 77 | console.log(chalk.redBright(error)) 78 | } 79 | } 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /mock/resource.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | 3 | module.exports = [{ 4 | url: '/sys/listResources', 5 | type: 'post', 6 | response: config => { 7 | return { 8 | ...Mock.mock({ 9 | version: 0, 10 | errorCode: 0, 11 | message: 'success', 12 | data: { 13 | total: 1000, 14 | 'resourceDetails|10': [{ 15 | id: '@id', 16 | path: config.query.path ? config.query.path + '.@word(3,5)' : '@pick([\'payment\',\'load\',\'resource\']).@pick([\'bcos\',\'bcos_gm\',\'fabric\']).@word(3,5)', 17 | checksum: 'checksum', 18 | 'stubType|1': ['BCOS2.0', 'GM_BCOS2.0', 'Fabric1.4', 'BCOS3_ECDSA_EVM', 'BCOS3_GM_EVM'], 19 | properties: '@sentence(3,3)', 20 | distance: '@integer(0, 3)' 21 | }] 22 | } 23 | }) 24 | } 25 | } 26 | }, 27 | { 28 | url: '/customCommand', 29 | type: 'post', 30 | response: config => { 31 | return { 32 | ...Mock.mock({ 33 | 'version': 1, 34 | 'errorCode': 0, 35 | 'message': 'Success: ' + config.body.command, 36 | 'data': 'result' 37 | }) 38 | } 39 | } 40 | }, 41 | { 42 | url: '/detail', 43 | type: 'post', 44 | response: config => { 45 | return { 46 | ...Mock.mock({ 47 | 'version': 1, 48 | 'errorCode': 0, 49 | 'message': 'Success: ' + config.body.command, 50 | 'data': { 51 | 'stubType': 'BCOS2.0' 52 | } 53 | }) 54 | } 55 | } 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /mock/status.js: -------------------------------------------------------------------------------- 1 | module.exports = [{ 2 | url: '/sys/systemStatus', 3 | type: 'get', 4 | response: param => { 5 | return { 6 | 'version': '1', 7 | 'errorCode': 0, 8 | 'message': 'Success', 9 | 'data': { 10 | 'osName': 'Linux', 11 | 'osArch': 'amd64', 12 | 'osVersion': '4.4.0-17763-Microsoft', 13 | 'javaVMVersion': '11.0.9.1+1-Ubuntu-0ubuntu1.20.04', 14 | 'javaVMVendor': 'Ubuntu', 15 | 'javaVMName': 'OpenJDK 64-Bit Server VM', 16 | 'providerName': 'SUN', 17 | 'providerVersion': '11', 18 | 'providerInfo': 'SUN (DSA key/parameter generation; DSA signing; SHA-1, MD5 digests; SecureRandom; X.509 certificates; PKCS12, JKS & DKS keystores; PKIX CertPathValidator; PKIX CertPathBuilder; LDAP, Collection CertStores, JavaPolicy Policy; JavaLoginConfig Configuration)', 19 | 'namedGroups': 'secp256k1', 20 | 'disabledNamedGroups': null 21 | } 22 | } 23 | } 24 | }, 25 | { 26 | url: '/sys/routerStatus', 27 | type: 'get', 28 | response: param => { 29 | return { 30 | 'version': '1', 31 | 'errorCode': 0, 32 | 'message': 'Success', 33 | 'data': { 34 | 'version': '1.0.0', 35 | 'supportedStubs': 'BCOS2.0,GM_BCOS2.0', 36 | 'rpcNetInfo': '127.0.0.1:8250', 37 | 'p2pNetInfo': '0.0.0.0:25500', 38 | 'adminAccount': 'monan', 39 | 'enableAccessControl': true 40 | } 41 | } 42 | } 43 | } 44 | ] 45 | -------------------------------------------------------------------------------- /mock/ua.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | 3 | module.exports = [ 4 | { 5 | url: '/auth/listAccount', 6 | type: 'post', 7 | response: _ => { 8 | return { 9 | ...Mock.mock({ 10 | version: 0, 11 | errorCode: 0, 12 | message: 'success', 13 | data: { 14 | 'username': 'org1-admin', 15 | 'uaID': '3059301306072a8648ce3d020106082a811ccf5501822d034200047cfc7f4488a171e4a80051cdf93e2febc3066181b17bccd81264b79e346affc1f684738aa565485a459bbc00f03bd1df3df7dac985e6a740a3ed5533d5a60874', 16 | 'pubKey': '3059301306072a8648ce3d020106082a811ccf5501822d034200047cfc7f4488a171e4a80051cdf93e2febc3066181b17bccd81264b79e346affc1f684738aa565485a459bbc00f03bd1df3df7dac985e6a740a3ed5533d5a60874', 17 | 'admin': true, 18 | 'version': 1, 19 | 'chainAccounts': [ 20 | { 21 | 'keyID': 0, 22 | 'identity': '0x6c51a6cef228f784636c690d8b13f956e177cc76', 23 | 'type': 'BCOS2.0', 24 | 'pubKey': '-----BEGIN PUBLIC KEY-----\nmock 1111\n-----END PUBLIC KEY-----\n', 25 | 'secKey': '-----BEGIN PRIVATE KEY-----\nmock xxxx\n-----END PRIVATE KEY-----\n', 26 | 'ext': '0x6c51a6cef228f784636c690d8b13f956e177cc76', 27 | 'isDefault': true 28 | }, 29 | { 30 | 'keyID': 3, 31 | 'identity': '-----BEGIN CERTIFICATE-----\nmock 2222\n-----END CERTIFICATE-----\n', 32 | 'type': 'Fabric1.4', 33 | 'pubKey': '-----BEGIN CERTIFICATE-----\nmock 2222\n-----END CERTIFICATE-----\n', 34 | 'secKey': '-----BEGIN PRIVATE KEY-----\nmock xxxx\n-----END PRIVATE KEY-----\n', 35 | 'ext': 'Org1MSP', 36 | 'isDefault': false 37 | }, 38 | { 39 | 'keyID': 2, 40 | 'identity': '-----BEGIN CERTIFICATE-----\nmock 3333\n-----END CERTIFICATE-----\n', 41 | 'type': 'Fabric1.4', 42 | 'pubKey': '-----BEGIN CERTIFICATE-----\nmock 3333\n-----END CERTIFICATE-----\n', 43 | 'secKey': '-----BEGIN PRIVATE KEY-----\nmock xxxx\n-----END PRIVATE KEY-----\n', 44 | 'ext': 'Org2MSP', 45 | 'isDefault': false 46 | }, 47 | { 48 | 'keyID': 1, 49 | 'identity': '-----BEGIN CERTIFICATE-----\nmock 4444\n-----END CERTIFICATE-----\n', 50 | 'type': 'Fabric1.4', 51 | 'pubKey': '-----BEGIN CERTIFICATE-----\nmock 4444\n-----END CERTIFICATE-----\n', 52 | 'secKey': '-----BEGIN PRIVATE KEY-----\nmock xxxx\n-----END PRIVATE KEY-----\n', 53 | 'ext': 'Org1MSP', 54 | 'isDefault': true 55 | } 56 | ] 57 | } 58 | }) 59 | } 60 | } 61 | }, 62 | { 63 | url: '/auth/setDefaultAccount', 64 | type: 'post', 65 | response: _ => { 66 | return { 67 | ...Mock.mock({ 68 | version: 0, 69 | errorCode: 0, 70 | message: 'success', 71 | data: { 72 | errorCode: 0, 73 | message: 'success' 74 | } 75 | }) 76 | } 77 | } 78 | }, 79 | { 80 | url: '/auth/addChainAccount', 81 | type: 'post', 82 | response: _ => { 83 | return { 84 | ...Mock.mock({ 85 | version: 0, 86 | errorCode: 0, 87 | message: 'success', 88 | data: { 89 | errorCode: 0, 90 | message: 'success' 91 | } 92 | }) 93 | } 94 | } 95 | }, 96 | { 97 | url: '/auth/removeChainAccount', 98 | type: 'post', 99 | response: _ => { 100 | return { 101 | ...Mock.mock({ 102 | version: 0, 103 | errorCode: 0, 104 | message: 'success', 105 | data: { 106 | errorCode: 0, 107 | message: 'success' 108 | } 109 | }) 110 | } 111 | } 112 | }, 113 | { 114 | url: '/auth/admin/accessControlList', 115 | type: 'get', 116 | response: _ => { 117 | return { 118 | ...Mock.mock({ 119 | version: 0, 120 | errorCode: 0, 121 | message: 'success', 122 | 'data|20': [ 123 | { 124 | 'username': '@id', 125 | 'allowChainPaths|20': [ 126 | 'payment.@id' 127 | ], 128 | 'updateTimestamp': 1624517049308 129 | } 130 | ] 131 | }) 132 | } 133 | } 134 | }, 135 | { 136 | url: '/auth/admin/accessControlList', 137 | type: 'post', 138 | response: _ => { 139 | return { 140 | ...Mock.mock({ 141 | version: 1, 142 | errorCode: 0, 143 | message: 'success', 144 | data: { 145 | errorCode: 0, 146 | message: 'success' 147 | } 148 | }) 149 | } 150 | } 151 | } 152 | ] 153 | -------------------------------------------------------------------------------- /mock/user.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | // user login 3 | { 4 | url: '/auth/login', 5 | type: 'post', 6 | response: _ => { 7 | const token = 'access-token' 8 | 9 | // mock error 10 | if (!token) { 11 | return { 12 | code: 60204, 13 | message: 'Account and password are incorrect.' 14 | } 15 | } 16 | 17 | return { 18 | 'version': '1.0', 19 | 'errorCode': 0, 20 | 'message': 'success', 21 | 'data': { 22 | 'errorCode': 0, 23 | 'message': 'success', 24 | 'credential': 'Bearer eyJpYXRtaWxsIjoxNjI0NDM3MDgwODk2LCJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJvcmcxLWFkbWluIiwibmJmIjoxNjI0NDM3MDgwLCJpc3MiOiJvcmcxIiwiZXhwIjoxNjI0NDM3NjgwLCJpYXQiOjE2MjQ0MzcwODB9.0yIjh54NvVgeoOX2DqbfTEm_ukdJeF7f44lii0Unkzc', 25 | 'universalAccount': { 26 | 'username': 'org1-admin', 27 | 'uaID': '3059301306072a8648ce3d020106082a811ccf5501822d03420004c52c0d26c2fc5368127472d9f7cad96d5792c3c6781f8b9cedaffd14c45deb6eabec7b7598f0ae194262bd1b8b5b4fbf75ade010b1ad688fa3ba7112a00c5d72', 28 | 'pubKey': '3059301306072a8648ce3d020106082a811ccf5501822d03420004c52c0d26c2fc5368127472d9f7cad96d5792c3c6781f8b9cedaffd14c45deb6eabec7b7598f0ae194262bd1b8b5b4fbf75ade010b1ad688fa3ba7112a00c5d72', 29 | 'isAdmin': true 30 | } 31 | } 32 | } 33 | } 34 | }, 35 | // user logout 36 | { 37 | url: '/auth/logout', 38 | type: 'post', 39 | response: _ => { 40 | return { 41 | 'version': '1', 42 | 'errorCode': 0, 43 | 'message': 'xxx', 44 | 'data': { 45 | } 46 | } 47 | } 48 | }, 49 | 50 | // user register 51 | { 52 | url: '/auth/register', 53 | type: 'post', 54 | response: _ => { 55 | return { 56 | 'version': '1', 57 | 'errorCode': 0, 58 | 'message': 'xxx', 59 | 'data': { 60 | 'errorCode': 0, 61 | 'message': 'success', 62 | 'universalAccount': { 63 | 'username': 'xxx', 64 | 'pubKey': 'xxx', 65 | 'uaID': 'xxx' 66 | } 67 | } 68 | } 69 | } 70 | }, 71 | 72 | // user changePassword 73 | { 74 | url: '/auth/changePassword', 75 | type: 'post', 76 | response: _ => { 77 | return { 78 | 'version': '1', 79 | 'errorCode': 0, 80 | 'message': 'xxx', 81 | 'data': { 82 | 'errorCode': 0, 83 | 'message': 'success' 84 | } 85 | } 86 | } 87 | }, 88 | 89 | // user authCode 90 | { 91 | url: '/auth/authCode', 92 | type: 'get', 93 | response: _ => { 94 | return { 95 | version: '1.0', 96 | errorCode: 0, 97 | message: 'success', 98 | data: { 99 | errorCode: 0, 100 | message: 'success', 101 | authCode: { 102 | randomToken: 'ad4b480b9585eaee7368a8260e28a198119bb88073f6f3b1aa03ede49ef1214e', 103 | imageBase64: 'iVBORw0KGgoAAAANSUhEUgAAAJsAAAA8CAIAAAD+Gl+NAAADQUlEQVR42u3cMW7kMAwFUB1im9S5xQLpUi9yhhxiq82Vttw+N0q53WSAAQzDGtGfFClSGgpqknFsj58lk7Sd8vX/kn2lXi6N9u/zL/FjNqN2Pc6yvq2hIJyJOkyxf20M0XQNPkxvPUVXa0Ugl6gpmi2AaIZIa4pmGrOgqEWEnc1fNFuKZkvRbCmaLUVTdJ4mvllRt/ffP/c9Rc/b25/XQwf/8OXXE9gF+dgB8m5P0dCoxLic1zVF2ZzBUT2vo1zUbZIMghrTdRrR7WqHc+KiMs4UlYvuQ9Mxorj6o4se2EDUQ7LRGQSJUVuurA19PP+49YlFa7YNDxGtc0dHUdaStWKrLyJKSNOiNerhN1qoBBjrruKpqKJricaJiFrko0h8JBMFObVQI4oqopqWGkBUlmi/q5so8ik+6yoWBdUTGBpsMlFk/LmI0oV7Lip9A+AUTHfi9Rmj3NwUR6W995Z2xd56E2OiXDdR2WKnl1LEW/bYm3gG3lwXj3W1RAU1ffWcFcxzrn1YuOuTj4oX47pa1CJ66oIWES/0JlNYURy1dZdGsXjUCoy1khlQ0aeuq4tKXFbrBexQb5sQV3rFrnRk5yYqiHjrL9bSou/VyKbT1iklTlL36wFFkSg9kCiozgpoZdUG4gIJPpoEjsj6/COW6Xoj2MUVrAhqJaa6ySg4RokhSAxWVg7tL8qt8Q6oCI4UPe3c8N7hjre6qEXhXlbj1c1e6MgoxKwrdtVF5T6iPaB2j4iCj48HerqTeLahp6SgVV5QfJYMryIJXtt1fktCxnZ6vOxKRZ3Pj9U2BGTcKuD4FvBFdALp7kBMUSpkCMtJQ8r2vKxk2Rqpjq6tHaN3uAe1LGnZOuUHu7YsWVeK6PnoSM7WQRlGy9ouvUv4Ny1BVASHmPUn9SasUfHN4V8fuR4Xd06xUP+YtkMFOcWnMhEilCCcrEPcKbE/oy1QTy/eKkF4aw0lDieopWhgkd7Qc4DW5qYZo50fRaAlkkvFs4cY9yUg58Xjf8Fuk3BndUJrPeJDN5moaYBKh5GK/13H7rh1ifbstCCJvAyp1vZHKwMmkhQdLeo4QOWi1pyd2aoX6oOKLow68aw7XtS9yD7LGP0G/T53V67Tc1kAAAAASUVORK5CYII=' 104 | } 105 | } 106 | } 107 | } 108 | }, 109 | // user pub 110 | { 111 | url: '/auth/pub', 112 | type: 'get', 113 | response: _ => { 114 | return { 115 | version: '1.0', 116 | errorCode: 0, 117 | message: 'success', 118 | data: { 119 | errorCode: 0, 120 | message: 'success', 121 | pub: 'MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAiJ0u2c/Xze3Jv+beltOfoaavZtatVmldwctq3ItZZ5w60whzKiNdrsFcQZqj2PszLbwiYsC4fEFKQIcaTiHCXDSQ1Km25ay8/c+NKprl/ruevGB1pXDnQNZhQqoaghfzijTX2bl6DJqCnuSV46sCKgKfyKm3PNPcsMUxYWC1283an3cviUvdSnZyXHN++T2Otw77EVm55CNuDpX5MkOOIPTMSAxzweC9n9dJf5sGbmzqK2Yx8Jp/9vUA+jqR8ljqKJ7Q6bLVW3/xuqAN542U8a3dvPY3RVAkLVxgnl7UIfQl5PcBIxd4W3NZM6da/ZGmp76MAq/hxpceoU7DmQntkP9mX9ExtzcUTNFTm+LERwR530YRx4P7QB3EAAujEklZrlhXVwNa3phXnZRJWm4nuJ3qQB0I2VIw9q247aSLWEkoXQWu9CyRWzt7hmxgspwCYwsMdkvs0t8zv5L1LK0i8qXLHQCrasHkoJQ16+aztSDFmrAhJKtC4JN+ACnR1kMXAz/r2o3Y+pCO/2eBSDllsYSwCMRcgFwGvmutSD5dLes+zFZusxTRZ6vVnnnob+fOZ0NAdEDG9QY4UZoUxMjqSqM2db9jQ67QlcuMuEsc7uQ7T5mWlNORBnEVCz/UIjvFKnw7XnvGWcT/hKTPKYbgkqOJ/KQ05DoF/W3VHU+inPMCAwEAAQ==' 122 | } 123 | } 124 | } 125 | }, 126 | { 127 | url: '/auth/need-mail-auth', 128 | type: 'get', 129 | response: _ => { 130 | return { 131 | version: '1.0', 132 | errorCode: 0, 133 | message: 'success', 134 | data: true 135 | } 136 | } 137 | }, 138 | { 139 | url: '/auth/mail-code', 140 | type: 'post', 141 | response: _ => { 142 | return { 143 | version: '1.0', 144 | errorCode: 0, 145 | message: 'success' 146 | } 147 | } 148 | } 149 | ] 150 | -------------------------------------------------------------------------------- /mock/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} url 3 | * @returns {Object} 4 | */ 5 | function param2Obj(url) { 6 | const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') 7 | if (!search) { 8 | return {} 9 | } 10 | const obj = {} 11 | const searchArr = search.split('&') 12 | searchArr.forEach(v => { 13 | const index = v.indexOf('=') 14 | if (index !== -1) { 15 | const name = v.substring(0, index) 16 | const val = v.substring(index + 1, v.length) 17 | obj[name] = val 18 | } 19 | }) 20 | return obj 21 | } 22 | 23 | module.exports = { 24 | param2Obj 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wecross-webapp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vue-cli-service serve", 7 | "build:prod": "vue-cli-service build", 8 | "build:stage": "vue-cli-service build --mode staging", 9 | "lint": "eslint --ext .js,.vue src", 10 | "new": "plop", 11 | "test:unit": "jest --clearCache && vue-cli-service test:unit", 12 | "test:ci": "npm run lint && npm run test:unit" 13 | }, 14 | "dependencies": { 15 | "axios": "^0.21.3", 16 | "body-parser": "^1.19.0", 17 | "chalk": "^4.1.0", 18 | "chokidar": "^3.5.1", 19 | "clipboard": "^2.0.6", 20 | "connect": "^3.7.0", 21 | "core-js": "3.7.0", 22 | "echarts": "^4.9.0", 23 | "element-ui": "2.13.2", 24 | "elliptic": "^6.5.4", 25 | "fuse.js": "^6.4.6", 26 | "intro.js": "^3.2.1", 27 | "js-cookie": "2.2.0", 28 | "js-sha256": "^0.9.0", 29 | "js-sha3": "0.8.0", 30 | "jsencrypt": "^3.0.0-rc.1", 31 | "jsonlint": "1.6.3", 32 | "jszip": "^3.8.0", 33 | "normalize.css": "7.0.0", 34 | "nprogress": "0.2.0", 35 | "path-to-regexp": "2.4.0", 36 | "script-loader": "0.7.2", 37 | "sm-crypto": "0.2.1", 38 | "sortablejs": "1.8.4", 39 | "svg-sprite-loader": "^5.0.0", 40 | "uuid": "^8.3.1", 41 | "vue": "^2.6.11", 42 | "vue-count-to": "1.0.13", 43 | "vue-json-pretty": "^1.7.1", 44 | "vue-router": "3.0.2", 45 | "vue-splitpane": "1.0.4", 46 | "vue-vis-network": "1.0.2", 47 | "vuex": "3.1.0" 48 | }, 49 | "devDependencies": { 50 | "@vue/cli-plugin-babel": "~4.5.0", 51 | "@vue/cli-plugin-eslint": "~4.5.0", 52 | "@vue/cli-plugin-unit-jest": "4.4.4", 53 | "@vue/cli-service": "~4.5.0", 54 | "@vue/test-utils": "1.0.0-beta.29", 55 | "babel-eslint": "^10.1.0", 56 | "babel-jest": "^26.6.3", 57 | "babel-plugin-dynamic-import-node": "2.3.3", 58 | "eslint": "^6.7.2", 59 | "eslint-plugin-vue": "^6.2.2", 60 | "eslint-plugin-vue-libs": "^4.0.0", 61 | "html-webpack-plugin": "3.2.0", 62 | "husky": "1.3.1", 63 | "lint-staged": "8.1.5", 64 | "mockjs": "1.0.1-beta3", 65 | "plop": "2.3.0", 66 | "runjs": "4.3.2", 67 | "sass": "1.26.2", 68 | "sass-loader": "8.0.2", 69 | "script-ext-html-webpack-plugin": "2.1.3", 70 | "serve-static": "1.13.2", 71 | "vue-template-compiler": "^2.6.11" 72 | }, 73 | "engines": { 74 | "node": ">=8.10", 75 | "npm": ">= 6.4.1" 76 | }, 77 | "husky": { 78 | "hooks": { 79 | "pre-commit": "lint-staged" 80 | } 81 | }, 82 | "lint-staged": { 83 | "src/**/*.{js,vue}": [ 84 | "eslint --fix", 85 | "git add" 86 | ] 87 | }, 88 | "browserslist": [ 89 | "> 1%", 90 | "last 2 versions", 91 | "not dead" 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /plop-templates/component/index.hbs: -------------------------------------------------------------------------------- 1 | {{#if template}} 2 | 3 | 4 | 5 | {{/if}} 6 | 7 | {{#if script}} 8 | 20 | {{/if}} 21 | 22 | {{#if style}} 23 | 26 | {{/if}} 27 | -------------------------------------------------------------------------------- /plop-templates/component/prompt.js: -------------------------------------------------------------------------------- 1 | const { notEmpty } = require('../utils.js') 2 | 3 | module.exports = { 4 | description: 'generate vue component', 5 | prompts: [{ 6 | type: 'input', 7 | name: 'name', 8 | message: 'component name please', 9 | validate: notEmpty('name') 10 | }, 11 | { 12 | type: 'checkbox', 13 | name: 'blocks', 14 | message: 'Blocks:', 15 | choices: [{ 16 | name: '', 17 | value: 'template', 18 | checked: true 19 | }, 20 | { 21 | name: ' 20 | {{/if}} 21 | 22 | {{#if style}} 23 | 26 | {{/if}} 27 | -------------------------------------------------------------------------------- /plop-templates/view/prompt.js: -------------------------------------------------------------------------------- 1 | const { notEmpty } = require('../utils.js') 2 | 3 | module.exports = { 4 | description: 'generate a view', 5 | prompts: [{ 6 | type: 'input', 7 | name: 'name', 8 | message: 'view name please', 9 | validate: notEmpty('name') 10 | }, 11 | { 12 | type: 'checkbox', 13 | name: 'blocks', 14 | message: 'Blocks:', 15 | choices: [{ 16 | name: '', 17 | value: 'template', 18 | checked: true 19 | }, 20 | { 21 | name: ' 12 | -------------------------------------------------------------------------------- /src/api/common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} ChainErrorMessage 3 | * @property {string} chain 4 | * @property {string} message 5 | * 6 | */ 7 | /** 8 | * @typedef {Object} ResourceDetail 9 | * @property {string} path - the path of resource 10 | * @property {number} distance - the distance of this resource 11 | * @property {Object} properties 12 | * @property {string} checksum 13 | */ 14 | /** 15 | * @typedef {Object} CallRequest 16 | * @property {string} version - the version of request 17 | * @property {string|*} path 18 | * @property {Object} data - the main body data of request 19 | * @property {string|*} data.method - call method 20 | * @property {Array|*} args - call args 21 | * @property {Object|*} options - sendTransaction options 22 | */ 23 | /** 24 | * @typedef {Object} CustomRequest 25 | * @property {string} version - the version of request 26 | * @property {string} path 27 | * @property {Object} data - the main body data of request 28 | * @property {string} data.command - the command of request 29 | * @property {Array|null} data.args - the args of command 30 | */ 31 | /** 32 | * @typedef {Object} xaRequest 33 | * @property {string} version - the version of request 34 | * @property {Object} data - the main body data of request 35 | * @property {string|null} data.xaTransactionID - the xa transaction ID 36 | * @property {Array|null} data.paths - the paths of xa transaction 37 | */ 38 | /** 39 | * @typedef {Object} Response 40 | * @property {string} version - the version of response 41 | * @property {number} errorCode - the system layer error code, 0 means success 42 | * @property {string} message 43 | * @property {Object|string} data - the main body data of response 44 | */ 45 | /** 46 | * @typedef {Object} resourceResponse 47 | * @property {string} version - the version of response 48 | * @property {number} errorCode - the system layer error code, 0 means success 49 | * @property {string} message 50 | * @property {Object|string} data - the main body data of response 51 | * @property {number} data.total - the total number of 'listResources' 52 | * @property {Array} data.resourceDetails - list of resources in 'listResources' 53 | */ 54 | /** 55 | * @typedef {Object} xaResponse 56 | * @property {string} version - the version of response 57 | * @property {number} errorCode - the system layer error code, 0 means success 58 | * @property {string} message 59 | * @property {Object} data - the main body data of response 60 | * @property {number} data.status - the status of this XA transaction 61 | * @property {Array|null} chainErrorMessages 62 | */ 63 | /** 64 | * @typedef {Object} xaListResponse 65 | * @property {string} version - the version of response 66 | * @property {number} errorCode - the system layer error code, 0 means success 67 | * @property {string} message 68 | * @property {Object} data - the main body data of response 69 | * @property {Object} data.xaResponse 70 | * @property {number} data.xaResponse.status 71 | * @property {Array|null} chainErrorMessages 72 | * @property {Object} xaTransaction 73 | */ 74 | -------------------------------------------------------------------------------- /src/api/conn.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * list peer routers 5 | * @param {Object|null} params - params to request: {offset,size} 6 | * @param {number} params.offset - the query page offset 7 | * @param {number} params.size - the page size of query 8 | * @return {Promise} an axios promise object of response 9 | */ 10 | export function listPeers(params) { 11 | return request({ 12 | url: '/conn/listPeers', 13 | method: 'get', 14 | params: params 15 | }) 16 | } 17 | 18 | /** 19 | * add peer router 20 | * @param {Object} data - body data to request: {version, data:{address}} 21 | * @return {Promise} an axios promise object of response 22 | */ 23 | export function addPeer(data) { 24 | return request({ 25 | url: '/conn/addPeer', 26 | method: 'post', 27 | data: data 28 | }) 29 | } 30 | 31 | /** 32 | * remove peer router 33 | * @param {Object} data - body data to request: {version, data:{address}} 34 | * @return {Promise} an axios promise object of response 35 | */ 36 | export function removePeer(data) { 37 | return request({ 38 | url: '/conn/removePeer', 39 | method: 'post', 40 | data: data 41 | }) 42 | } 43 | 44 | /** 45 | * get a list of chains in network 46 | * @param {Object} params - params to request: {zone,offset,size}, if offset & size is null then return all list 47 | * @param {string} params.zone - the zone which chains on 48 | * @param {number|null} params.offset - the query page offset 49 | * @param {number|null} params.size - the page size of query 50 | * @return {Promise} an axios promise object of response 51 | */ 52 | export function listChains(params) { 53 | return request({ 54 | url: '/conn/listChains', 55 | method: 'get', 56 | params: params 57 | }) 58 | } 59 | 60 | /** 61 | * get a list of zones in network 62 | * @param {Object|null} params - params to request: {offset,size}, if params is null then return all list 63 | * @param {number} params.offset - the query page offset 64 | * @param {number} params.size - the page size of query 65 | * @return {Promise} an axios promise object of response 66 | */ 67 | export function listZones(params) { 68 | return request({ 69 | url: '/conn/listZones', 70 | method: 'get', 71 | params: params 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /src/api/resource.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { path2Url } from '@/utils' 3 | 4 | /** 5 | * get a list of resources 6 | * @param {Object|null} params - params to request 7 | * @param {string|null} params.path - the path which resources on 8 | * @param {number} params.offset - the query page offset 9 | * @param {number} params.size - the page size of query 10 | * @param {Object} data - Body data to request: {version, data:{ignoreRemote}} 11 | * @return {Promise} an axios promise object of response 12 | */ 13 | export function getResourceList(params, data) { 14 | return request({ 15 | url: '/sys/listResources', 16 | method: 'post', 17 | params: params, 18 | data: data 19 | }) 20 | } 21 | 22 | /** 23 | * get a detail info of a resource 24 | * @param {String} path - request path 25 | * @return {Promise} an axios promise object of response 26 | */ 27 | export function detail(path) { 28 | return request({ 29 | url: 'resource' + path2Url(path) + '/detail', 30 | method: 'post', 31 | data: { version: 1, data: null } 32 | }) 33 | } 34 | 35 | /** 36 | * deploy a BCOS contract 37 | * @param {CustomRequest|*} data - body data to request 38 | * @return {Promise} an axios promise object of response 39 | */ 40 | export function bcosDeploy(data) { 41 | return request({ 42 | url: 'resource' + path2Url(data.path) + '/customCommand', 43 | method: 'post', 44 | data: data 45 | }) 46 | } 47 | 48 | /** 49 | * register a BCOS contract 50 | * @param {CustomRequest|*} data - body data to request 51 | * @return {Promise} an axios promise object of response 52 | */ 53 | export function bcosRegister(data) { 54 | return request({ 55 | url: 'resource' + path2Url(data.path) + '/customCommand', 56 | method: 'post', 57 | data: data 58 | }) 59 | } 60 | 61 | /** 62 | * install a Fabric chaincode 63 | * @param {CustomRequest|*} data - body data to request 64 | * @return {Promise} an axios promise object of response 65 | */ 66 | export function fabricInstall(data) { 67 | return request({ 68 | url: 'resource' + path2Url(data.path) + '/customCommand', 69 | method: 'post', 70 | data: data 71 | }) 72 | } 73 | 74 | /** 75 | * instantiate a Fabric chaincode 76 | * @param {CustomRequest|*} data - body data to request 77 | * @return {Promise} an axios promise object of response 78 | */ 79 | export function fabricInstantiate(data) { 80 | return request({ 81 | url: 'resource' + path2Url(data.path) + '/customCommand', 82 | method: 'post', 83 | data: data 84 | }) 85 | } 86 | 87 | /** 88 | * upgrade a Fabric chaincode 89 | * @param {CustomRequest|*} data - body data to request 90 | * @return {Promise} an axios promise object of response 91 | */ 92 | export function fabricUpgrade(data) { 93 | return request({ 94 | url: 'resource' + path2Url(data.path) + '/customCommand', 95 | method: 'post', 96 | data: data 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /src/api/status.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * get the status of system 5 | * @return {Promise} an axios promise object of response 6 | */ 7 | export function systemStatus() { 8 | return request({ 9 | url: '/sys/systemStatus', 10 | method: 'get' 11 | }) 12 | } 13 | 14 | /** 15 | * list supported stub types in wecross 16 | * @return {Promise} an axios promise object of response 17 | */ 18 | export function supportedStubs(params) { 19 | return request({ 20 | url: '/sys/supportedStubs', 21 | method: 'get', 22 | params 23 | }) 24 | } 25 | 26 | /** 27 | * get wecross router status 28 | * @return {Promise} an axios promise object of response 29 | */ 30 | export function routerStatus() { 31 | return request({ 32 | url: '/sys/routerStatus', 33 | method: 'get' 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /src/api/transaction.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { path2Url } from '@/utils' 3 | 4 | /** 5 | * start a XA transaction 6 | * @param {xaRequest} data - body data to request 7 | * @return {Promise} an axios promise object of response 8 | */ 9 | export function startXATransaction(data) { 10 | return request({ 11 | url: '/xa/startXATransaction', 12 | method: 'post', 13 | data: data 14 | }) 15 | } 16 | 17 | /** 18 | * commit a XA transaction 19 | * @param {xaRequest} data - body data to request 20 | * @return {Promise} an axios promise object of response 21 | */ 22 | export function commitXATransaction(data) { 23 | return request({ 24 | url: '/xa/commitXATransaction', 25 | method: 'post', 26 | data: data 27 | }) 28 | } 29 | 30 | /** 31 | * rollback a XA transaction 32 | * @param {xaRequest} data - body data to request 33 | * @return {Promise} an axios promise object of response 34 | */ 35 | export function rollbackXATransaction(data) { 36 | return request({ 37 | url: '/xa/rollbackXATransaction', 38 | method: 'post', 39 | data: data 40 | }) 41 | } 42 | 43 | /** 44 | * get a XA transaction's info 45 | * @param {xaRequest|*} data - body data to request 46 | * @return {Promise} an axios promise object of response 47 | */ 48 | export function getXATransaction(data) { 49 | return request({ 50 | url: '/xa/getXATransaction', 51 | method: 'post', 52 | data: data 53 | }) 54 | } 55 | 56 | /** 57 | * get a XA transaction's info 58 | * @param {Object} data - body data to request 59 | * @param {string} data.version 60 | * @param {number} data.data.size 61 | * @param {Map|*} data.data.offsets - path => number 62 | * @return {Promise} an axios promise object of response 63 | */ 64 | export function listXATransactions(data) { 65 | return request({ 66 | url: '/xa/listXATransactions', 67 | method: 'post', 68 | data: data 69 | }) 70 | } 71 | 72 | /** 73 | * call a contract status 74 | * @param {CallRequest} data 75 | * @return {Promise} an axios promise object of response 76 | */ 77 | export function call(data) { 78 | return request({ 79 | url: 'resource' + path2Url(data.path) + '/call', 80 | method: 'post', 81 | data: data 82 | }) 83 | } 84 | 85 | /** 86 | * send a contract transaction 87 | * @param {CallRequest} data 88 | * @return {Promise} an axios promise object of response 89 | */ 90 | export function sendTransaction(data) { 91 | return request({ 92 | url: 'resource' + path2Url(data.path) + '/sendTransaction', 93 | method: 'post', 94 | data: data 95 | }) 96 | } 97 | 98 | /** 99 | * list transactions of path 100 | * @param {Object} params 101 | * @param {string} params.path 102 | * @param {number} params.blockNumber 103 | * @param {number} params.offset 104 | * @param {number} params.size 105 | * @return {Promise} an axios promise object of response 106 | */ 107 | export function listTransactions(params) { 108 | return request({ 109 | url: '/trans/listTransactions', 110 | method: 'get', 111 | params: params 112 | }) 113 | } 114 | 115 | /** 116 | * get a exact transaction's info by hash 117 | * @param {Object} params 118 | * @param {string} params.path 119 | * @param {string} params.txHash 120 | * @return {Promise} an axios promise object of response 121 | */ 122 | export function getTransaction(params) { 123 | return request({ 124 | url: '/trans/getTransaction', 125 | method: 'get', 126 | params: params 127 | }) 128 | } 129 | -------------------------------------------------------------------------------- /src/api/ua.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * list all chain account of UA 5 | * @return {Promise} an axios promise object of response 6 | */ 7 | export function listAccount() { 8 | return request({ 9 | url: '/auth/listAccount', 10 | method: 'post', 11 | data: {} 12 | }) 13 | } 14 | 15 | /** 16 | * set default chain account by keyID 17 | * @param {Object} data 18 | * @param {string} data.version 19 | * @param {Object} data.data - main body data of request 20 | * @param {string} data.data.type - the chain type 21 | * @param {number} data.data.keyID 22 | * @return {Promise} an axios promise object of response 23 | */ 24 | export function setDefaultAccount(data) { 25 | return request({ 26 | url: '/auth/setDefaultAccount', 27 | method: 'post', 28 | data: data 29 | }) 30 | } 31 | 32 | /** 33 | * add a chain account to UA 34 | * @param {Object} data 35 | * @param {string} data.version 36 | * @param {Object} data.data - main body data of request 37 | * @param {string} data.data.type - the chain type 38 | * @param {string} data.data.pubKey 39 | * @param {string} data.data.secKey 40 | * @param {string} data.data.ext 41 | * @param {boolean} data.data.isDefault 42 | * @return {Promise} an axios promise object of response 43 | */ 44 | export function addChainAccount(data) { 45 | return request({ 46 | url: '/auth/addChainAccount', 47 | method: 'post', 48 | data: data 49 | }) 50 | } 51 | 52 | /** 53 | * remove a chain account by keyID 54 | * @param {Object} data 55 | * @param {string} data.version 56 | * @param {Object} data.data - main body data of request 57 | * @param {string} data.data.type - the chain type 58 | * @param {number} data.data.keyID 59 | * @return {Promise} an axios promise object of response 60 | */ 61 | export function removeChainAccount(data) { 62 | return request({ 63 | url: '/auth/removeChainAccount', 64 | method: 'post', 65 | data: data 66 | }) 67 | } 68 | 69 | /** 70 | * get all access chain of username 71 | * @param {Object|null} params param include username, if null get all user 72 | * @param {String} params.username username 73 | * @return {Promise} an axios promise object of response 74 | */ 75 | export function accessControlListGet(params) { 76 | return request({ 77 | url: '/auth/admin/accessControlList', 78 | method: 'get', 79 | params: params 80 | }) 81 | } 82 | 83 | /** 84 | * post access chain of username 85 | * @param {Object} params param include username, if null get all user 86 | * @param {String} params.username username 87 | * @param {Object} data 88 | * @param {String} data.version 89 | * @param {Object} data.data 90 | * @param {Array[String]} data.data.allowChainPaths 91 | * @return {Promise} an axios promise object of response 92 | */ 93 | export function accessControlListPost(params, data) { 94 | return request({ 95 | url: '/auth/admin/accessControlList', 96 | method: 'post', 97 | params: params, 98 | data: data 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * login 5 | * @param {string} data - encoded login params 6 | * @return {Promise} an axios promise object of response 7 | */ 8 | export function login(data) { 9 | return request({ 10 | url: '/auth/login', 11 | method: 'post', 12 | data: { 13 | version: '1', 14 | data: data 15 | } 16 | }) 17 | } 18 | 19 | /** 20 | * logout 21 | * @return {Promise} an axios promise object of response 22 | */ 23 | export function logout() { 24 | return request({ 25 | url: '/auth/logout', 26 | method: 'post', 27 | data: {} 28 | }) 29 | } 30 | 31 | /** 32 | * register 33 | * @param {string} data - encoded register params 34 | * @return {Promise} an axios promise object of response 35 | */ 36 | export function register(data) { 37 | return request({ 38 | url: '/auth/register', 39 | method: 'post', 40 | data: { 41 | version: '1', 42 | data: data 43 | } 44 | }) 45 | } 46 | 47 | /** 48 | * changePassword 49 | * @param {string} data - encoded changePassword params 50 | * @return {Promise} an axios promise object of response 51 | */ 52 | export function changePassword(data) { 53 | return request({ 54 | url: '/auth/changePassword', 55 | method: 'post', 56 | data: { 57 | version: '1', 58 | data: data 59 | } 60 | }) 61 | } 62 | 63 | /** 64 | * get a auth code for login/register 65 | * @return {Promise} an axios promise object of response 66 | */ 67 | export function authCode() { 68 | return request({ 69 | url: '/auth/authCode', 70 | method: 'get' 71 | }) 72 | } 73 | 74 | /** 75 | * get a pubKey for login/register 76 | * @return {Promise} an axios promise object of response 77 | */ 78 | export function authPub() { 79 | return request({ 80 | url: '/auth/pub', 81 | method: 'get' 82 | }) 83 | } 84 | 85 | export function isNeedMailAuth() { 86 | return request({ 87 | url: '/auth/need-mail-auth', 88 | method: 'get' 89 | }) 90 | } 91 | 92 | export function sendMailCode(username, email) { 93 | return request({ 94 | url: '/auth/mail-code', 95 | method: 'post', 96 | data: { 97 | data: { 98 | username: username, 99 | email: email 100 | } 101 | } 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankBlockchain/WeCross-WebApp/ff0a2850a465d5e7167cdb70a03d45d19cbb4963/src/assets/404_images/404.png -------------------------------------------------------------------------------- /src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankBlockchain/WeCross-WebApp/ff0a2850a465d5e7167cdb70a03d45d19cbb4963/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /src/assets/QRCode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankBlockchain/WeCross-WebApp/ff0a2850a465d5e7167cdb70a03d45d19cbb4963/src/assets/QRCode.jpg -------------------------------------------------------------------------------- /src/assets/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankBlockchain/WeCross-WebApp/ff0a2850a465d5e7167cdb70a03d45d19cbb4963/src/assets/app.png -------------------------------------------------------------------------------- /src/assets/chain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankBlockchain/WeCross-WebApp/ff0a2850a465d5e7167cdb70a03d45d19cbb4963/src/assets/chain.png -------------------------------------------------------------------------------- /src/assets/check-fail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | check-pass 9 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/check-pass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | check-pass 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | favicon 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/assets/icon-bc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icon-ly.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icon-sw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icon-zy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/issue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankBlockchain/WeCross-WebApp/ff0a2850a465d5e7167cdb70a03d45d19cbb4963/src/assets/issue.png -------------------------------------------------------------------------------- /src/assets/nav-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | nav-logo 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeBankBlockchain/WeCross-WebApp/ff0a2850a465d5e7167cdb70a03d45d19cbb4963/src/assets/router.png -------------------------------------------------------------------------------- /src/assets/wecross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | logo/横/原色 11 | Created with Sketch. 12 | 13 | 18 | 23 | 27 | 29 | 36 | 43 | 44 | 47 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ item.meta.title }} 6 | {{ item.meta.title }} 7 | 8 | 9 | 10 | 11 | 12 | 64 | 65 | 78 | -------------------------------------------------------------------------------- /src/components/ChainExplorer/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 138 | 139 | 141 | -------------------------------------------------------------------------------- /src/components/Clipboard/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 27 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /src/components/HeaderSearch/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 18 | 19 | 20 | 21 | 22 | 135 | 136 | 174 | -------------------------------------------------------------------------------- /src/components/ResourceShower/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | {{ scope.row.path }} 10 | 11 | 12 | 13 | {{ JSON.stringify(scope.row.properties) }} 14 | 15 | 16 | 17 | 26 | 27 | 28 | 29 | 91 | 92 | 94 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 46 | 47 | 62 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /src/icons/svg/chicken.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/exit-fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/qrcode.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/question.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/user-avatar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 37 | 38 | 50 | 51 | 59 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | device() { 4 | return this.$store.state.app.device 5 | } 6 | }, 7 | mounted() { 8 | this.fixBugIniOS() 9 | }, 10 | methods: { 11 | fixBugIniOS() { 12 | const $subMenu = this.$refs.subMenu 13 | if ($subMenu) { 14 | const handleMouseleave = $subMenu.handleMouseleave 15 | $subMenu.handleMouseleave = (e) => { 16 | if (this.device === 'mobile') { 17 | return 18 | } 19 | handleMouseleave(e) 20 | } 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 47 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 44 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ title }} 7 | 8 | 9 | 10 | 11 | 12 | 23 | 24 | 69 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 22 | 23 | 24 | 25 | 26 | 93 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 52 | -------------------------------------------------------------------------------- /src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from './Navbar' 2 | export { default as Sidebar } from './Sidebar' 3 | export { default as AppMain } from './AppMain' 4 | -------------------------------------------------------------------------------- /src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route() { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }).then(_ => {}) 11 | } 12 | } 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeDestroy() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile').then(_ => {}) 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }).then(_ => {}) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop').then(_ => {}) 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }).then(_ => {}) 41 | } else { 42 | store.dispatch('app/openSideBar', { withoutAnimation: false }).then(_ => {}) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import 'normalize.css/normalize.css' // A modern alternative to CSS resets 4 | 5 | import ElementUI from 'element-ui' 6 | import 'element-ui/lib/theme-chalk/index.css' 7 | 8 | import '@/styles/index.scss' // global css 9 | 10 | import App from './App' 11 | import store from './store' 12 | import router from './router' 13 | 14 | import '@/icons' // icon 15 | import '@/permission' // permission control 16 | 17 | Vue.use(ElementUI) 18 | 19 | Vue.config.productionTip = false 20 | 21 | new Vue({ 22 | el: '#app', 23 | router, 24 | store, 25 | render: h => h(App) 26 | }) 27 | -------------------------------------------------------------------------------- /src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import store from './store' 3 | import { Message } from 'element-ui' 4 | import NProgress from 'nprogress' // progress bar 5 | import 'nprogress/nprogress.css' // progress bar style 6 | import { getToken } from '@/utils/auth' // get token from cookie 7 | import getPageTitle from '@/utils/get-page-title' 8 | 9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration 10 | 11 | const whiteList = ['/login', '/register'] // no redirect whitelist 12 | 13 | router.beforeEach(async(to, from, next) => { 14 | // start progress bar 15 | NProgress.start() 16 | 17 | // set page title 18 | document.title = getPageTitle(to.meta.title) 19 | 20 | // determine whether the user has logged in 21 | const hasToken = getToken() 22 | 23 | if (hasToken) { 24 | if (to.path === '/login') { 25 | if (from.path === '/register') { 26 | await store.dispatch('user/resetToken') 27 | next({ path: '/login' }) 28 | NProgress.done() 29 | } else { 30 | // if is logged in, redirect to the home page 31 | next({ path: '/' }) 32 | NProgress.done() 33 | } 34 | } else { 35 | const hasRoles = store.getters.roles && store.getters.roles.length > 0 36 | if (hasRoles) { 37 | next() 38 | NProgress.done() 39 | } else { 40 | try { 41 | if (store.getters.permissionRoutes === null || store.getters.permissionRoutes.length === 0) { 42 | const roles = await store.dispatch('user/getRole') 43 | const accessRoutes = await store.dispatch('permission/generateRoutes', roles) 44 | router.addRoutes(accessRoutes) 45 | next({ ...to, replace: true }) 46 | NProgress.done() 47 | } else { 48 | next() 49 | NProgress.done() 50 | } 51 | } catch (error) { 52 | // remove token and go to login page to re-login 53 | await store.dispatch('user/resetToken') 54 | Message.error(error || 'Has Error') 55 | next(`/login?redirect=${to.path}`) 56 | NProgress.done() 57 | } 58 | } 59 | } 60 | } else { 61 | /* has no token*/ 62 | if (whiteList.indexOf(to.path) !== -1) { 63 | // in the free login whitelist, go directly 64 | next() 65 | } else { 66 | // other pages that do not have permission to access are redirected to the login page. 67 | next(`/login?redirect=${to.path}`) 68 | NProgress.done() 69 | } 70 | } 71 | }) 72 | 73 | router.afterEach(() => { 74 | // finish progress bar 75 | NProgress.done() 76 | }) 77 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | device: state => state.app.device, 4 | token: state => state.user.token, 5 | avatar: state => state.user.avatar, 6 | name: state => state.user.name, 7 | roles: state => state.user.roles, 8 | transactionID: state => state.transaction.transactionID, 9 | XAPaths: state => state.transaction.paths, 10 | permissionRoutes: state => state.permission.routes 11 | } 12 | export default getters 13 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | import app from './modules/app' 5 | import user from './modules/user' 6 | import transaction from './modules/transaction' 7 | import permission from '@/store/modules/permission' 8 | 9 | Vue.use(Vuex) 10 | 11 | const store = new Vuex.Store({ 12 | modules: { 13 | app, 14 | user, 15 | transaction, 16 | permission 17 | }, 18 | getters 19 | }) 20 | 21 | export default store 22 | -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const state = { 4 | sidebar: { 5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, 6 | withoutAnimation: false 7 | }, 8 | device: 'desktop' 9 | } 10 | 11 | const mutations = { 12 | TOGGLE_SIDEBAR: state => { 13 | state.sidebar.opened = !state.sidebar.opened 14 | state.sidebar.withoutAnimation = false 15 | if (state.sidebar.opened) { 16 | Cookies.set('sidebarStatus', 1) 17 | } else { 18 | Cookies.set('sidebarStatus', 0) 19 | } 20 | }, 21 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 22 | Cookies.set('sidebarStatus', 0) 23 | state.sidebar.opened = false 24 | state.sidebar.withoutAnimation = withoutAnimation 25 | }, 26 | OPEN_SIDEBAR: (state, withoutAnimation) => { 27 | Cookies.set('sidebarStatus', 1) 28 | state.sidebar.opened = true 29 | state.sidebar.withoutAnimation = withoutAnimation 30 | }, 31 | TOGGLE_DEVICE: (state, device) => { 32 | state.device = device 33 | } 34 | } 35 | 36 | const actions = { 37 | toggleSideBar({ commit }) { 38 | commit('TOGGLE_SIDEBAR') 39 | }, 40 | closeSideBar({ commit }, { withoutAnimation }) { 41 | commit('CLOSE_SIDEBAR', withoutAnimation) 42 | }, 43 | openSideBar({ commit }, { withoutAnimation }) { 44 | commit('OPEN_SIDEBAR', withoutAnimation) 45 | }, 46 | toggleDevice({ commit }, device) { 47 | commit('TOGGLE_DEVICE', device) 48 | } 49 | } 50 | 51 | export default { 52 | namespaced: true, 53 | state, 54 | mutations, 55 | actions 56 | } 57 | -------------------------------------------------------------------------------- /src/store/modules/permission.js: -------------------------------------------------------------------------------- 1 | import { asyncRoutes, constantRoutes } from '@/router' 2 | 3 | /** 4 | * Use meta.role to determine if the current user has permission 5 | * @param roles 6 | * @param route 7 | */ 8 | function hasPermission(roles, route) { 9 | if (route.meta && route.meta.roles) { 10 | return roles.some(role => route.meta.roles.includes(role)) 11 | } else { 12 | return true 13 | } 14 | } 15 | 16 | /** 17 | * Filter asynchronous routing tables by recursion 18 | * @param routes asyncRoutes 19 | * @param roles 20 | */ 21 | export function filterAsyncRoutes(routes, roles) { 22 | const res = [] 23 | 24 | routes.forEach(route => { 25 | const tmp = { ...route } 26 | if (hasPermission(roles, tmp)) { 27 | if (tmp.children) { 28 | tmp.children = filterAsyncRoutes(tmp.children, roles) 29 | } 30 | res.push(tmp) 31 | } 32 | }) 33 | 34 | return res 35 | } 36 | 37 | const state = { 38 | routes: [], 39 | addRoutes: [] 40 | } 41 | 42 | const mutations = { 43 | SET_ROUTES: (state, routes) => { 44 | state.addRoutes = routes 45 | state.routes = constantRoutes.concat(routes) 46 | }, 47 | RESET_ROUTES: (state) => { 48 | state.addRoutes = [] 49 | state.routes = [] 50 | } 51 | } 52 | 53 | const actions = { 54 | generateRoutes({ commit }, roles) { 55 | return new Promise(resolve => { 56 | let accessedRoutes 57 | if (roles.includes('admin')) { 58 | accessedRoutes = asyncRoutes || [] 59 | } else { 60 | accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) 61 | } 62 | commit('SET_ROUTES', accessedRoutes) 63 | resolve(accessedRoutes) 64 | }) 65 | }, 66 | resetRoutes({ commit }) { 67 | return new Promise(resolve => { 68 | commit('RESET_ROUTES') 69 | resolve() 70 | }) 71 | } 72 | } 73 | 74 | export default { 75 | namespaced: true, 76 | state, 77 | mutations, 78 | actions 79 | } 80 | -------------------------------------------------------------------------------- /src/store/modules/transaction.js: -------------------------------------------------------------------------------- 1 | import { commitXATransaction, startXATransaction, rollbackXATransaction } from '@/api/transaction' 2 | import { getXATX, removeXATX, setXATX, buildXAError } from '@/utils/transaction' 3 | import { handleErrorMsgBox } from '@/utils/messageBox' 4 | 5 | const getDefaultState = () => { 6 | return { 7 | transactionID: getXATX() ? getXATX().transactionID : null, 8 | paths: getXATX() ? getXATX().paths : [] 9 | } 10 | } 11 | const state = getDefaultState() 12 | 13 | const mutations = { 14 | RESET_STATE: (state) => { 15 | removeXATX() 16 | Object.assign(state, getDefaultState()) 17 | }, 18 | SET_TRANSACTION: (state, transaction) => { 19 | state.transactionID = transaction.transactionID 20 | state.paths = transaction.paths 21 | } 22 | } 23 | 24 | const actions = { 25 | startTransaction({ commit }, transaction) { 26 | return new Promise((resolve, reject) => { 27 | startXATransaction(transaction).then(response => { 28 | if (response.errorCode !== 0 || response.data.status !== 0) { 29 | const errMessage = buildXAError(response) 30 | handleErrorMsgBox('开启事务失败,错误:', '错误', errMessage, null).then(_ => {}) 31 | reject() 32 | } else { 33 | commit('SET_TRANSACTION', { transactionID: transaction.data.xaTransactionID, paths: transaction.data.paths }) 34 | setXATX(JSON.stringify({ transactionID: transaction.data.xaTransactionID, paths: transaction.data.paths })) 35 | resolve() 36 | } 37 | }).catch(error => { 38 | handleErrorMsgBox('开启事务失败,错误:', '错误', error.message, null).then(_ => {}) 39 | reject(error) 40 | }) 41 | }) 42 | }, 43 | commitTransaction({ commit }, transaction) { 44 | return new Promise((resolve, reject) => { 45 | commitXATransaction(transaction).then(response => { 46 | if (response.errorCode !== 0 || response.data.status !== 0) { 47 | const errMessage = buildXAError(response) 48 | handleErrorMsgBox('提交事务失败,错误:', '错误', errMessage, null) 49 | .then(_ => { 50 | if (/(committed|rolledback)/.test(errMessage)) { 51 | commit('RESET_STATE') 52 | removeXATX() 53 | } 54 | }) 55 | reject(errMessage) 56 | } else { 57 | commit('RESET_STATE') 58 | removeXATX() 59 | resolve() 60 | } 61 | }).catch(error => { 62 | handleErrorMsgBox('提交事务失败,错误:', '错误', error.message, null) 63 | .then(_ => {}) 64 | reject(error) 65 | }) 66 | }) 67 | }, 68 | rollbackTransaction({ commit }, transaction) { 69 | return new Promise((resolve, reject) => { 70 | rollbackXATransaction(transaction).then(response => { 71 | if (response.errorCode !== 0 || response.data.status !== 0) { 72 | const errMessage = buildXAError(response) 73 | handleErrorMsgBox('回滚事务失败,错误:', '错误', errMessage, null) 74 | .then(_ => { 75 | if (/(committed|rolledback)$/.test(errMessage)) { 76 | commit('RESET_STATE') 77 | removeXATX() 78 | } 79 | }) 80 | reject(errMessage) 81 | } else { 82 | commit('RESET_STATE') 83 | removeXATX() 84 | resolve() 85 | } 86 | }).catch(error => { 87 | handleErrorMsgBox('回滚事务失败,错误:', '错误', error.message, null) 88 | .then(_ => {}) 89 | reject(error) 90 | }) 91 | }) 92 | } 93 | } 94 | 95 | export default { 96 | namespaced: true, 97 | state, 98 | mutations, 99 | actions 100 | } 101 | -------------------------------------------------------------------------------- /src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | .cell { 19 | .el-tag { 20 | margin-right: 0px; 21 | } 22 | } 23 | 24 | .small-padding { 25 | .cell { 26 | padding-left: 5px; 27 | padding-right: 5px; 28 | } 29 | } 30 | 31 | .fixed-width { 32 | .el-button--mini { 33 | padding: 7px 10px; 34 | min-width: 60px; 35 | } 36 | } 37 | 38 | .status-col { 39 | .cell { 40 | padding: 0 10px; 41 | text-align: center; 42 | 43 | .el-tag { 44 | margin-right: 0px; 45 | } 46 | } 47 | } 48 | 49 | // to fixed https://github.com/ElemeFE/element/issues/2461 50 | .el-dialog { 51 | transform: none; 52 | left: 0; 53 | position: relative; 54 | margin: 0 auto; 55 | } 56 | 57 | // refine element ui upload 58 | .upload-container { 59 | .el-upload { 60 | width: 100%; 61 | 62 | .el-upload-dragger { 63 | width: 100%; 64 | height: 200px; 65 | } 66 | } 67 | } 68 | 69 | // dropdown 70 | .el-dropdown-menu { 71 | a { 72 | display: block 73 | } 74 | } 75 | 76 | // fix date-picker ui bug in filter-item 77 | .el-range-editor.el-input__inner { 78 | display: inline-flex !important; 79 | } 80 | 81 | // to fix el-date-picker css style 82 | .el-range-separator { 83 | box-sizing: content-box; 84 | } 85 | 86 | .el-form-item__label { 87 | padding: 0 10px 0 0; 88 | } 89 | 90 | .el-dropdown-menu__item { 91 | line-height: 30px; 92 | } 93 | 94 | .el-form--label-top .el-form-item__label { 95 | float: none; 96 | display: inline; 97 | text-align: left; 98 | padding: 0 0 10px; 99 | } 100 | 101 | .el-table__body-wrapper { 102 | overflow-y: auto; 103 | } 104 | -------------------------------------------------------------------------------- /src/styles/element-variables.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * I think element-ui's default theme color is too light for long-term use. 3 | * So I modified the default color and you can modify it to your liking. 4 | **/ 5 | 6 | /* theme color */ 7 | $--color-primary: #1890ff; 8 | $--color-success: #13ce66; 9 | $--color-warning: #ffba00; 10 | $--color-danger: #ff4949; 11 | // $--color-info: #1E1E1E; 12 | 13 | $--button-font-weight: 400; 14 | 15 | // $--color-text-regular: #1f2d3d; 16 | 17 | $--border-color-light: #dfe4ed; 18 | $--border-color-lighter: #e6ebf5; 19 | 20 | $--table-border: 1px solid #dfe6ec; 21 | 22 | /* icon font path, required */ 23 | $--font-path: "~element-ui/lib/theme-chalk/fonts"; 24 | 25 | @import "~element-ui/packages/theme-chalk/src/index"; 26 | 27 | // the :export directive is the magic sauce for webpack 28 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 29 | :export { 30 | theme: $--color-primary; 31 | } 32 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './mixin.scss'; 3 | @import './transition.scss'; 4 | @import './element-ui.scss'; 5 | @import './sidebar.scss'; 6 | 7 | body { 8 | height: 100%; 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-font-smoothing: antialiased; 11 | text-rendering: optimizeLegibility; 12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 13 | } 14 | 15 | label { 16 | font-weight: 700; 17 | } 18 | 19 | html { 20 | height: 100%; 21 | box-sizing: border-box; 22 | } 23 | 24 | #app { 25 | height: 100%; 26 | } 27 | 28 | *, 29 | *:before, 30 | *:after { 31 | box-sizing: inherit; 32 | } 33 | 34 | .no-padding { 35 | padding: 0px !important; 36 | } 37 | 38 | .padding-content { 39 | padding: 4px 0; 40 | } 41 | 42 | a:focus, 43 | a:active { 44 | outline: none; 45 | } 46 | 47 | a, 48 | a:focus, 49 | a:hover { 50 | cursor: pointer; 51 | color: inherit; 52 | text-decoration: none; 53 | } 54 | 55 | div:focus { 56 | outline: none; 57 | } 58 | 59 | .fr { 60 | float: right; 61 | } 62 | 63 | .fl { 64 | float: left; 65 | } 66 | 67 | .pr-5 { 68 | padding-right: 5px; 69 | } 70 | 71 | .pl-5 { 72 | padding-left: 5px; 73 | } 74 | 75 | .block { 76 | display: block; 77 | } 78 | 79 | .pointer { 80 | cursor: pointer; 81 | } 82 | 83 | .inlineBlock { 84 | display: block; 85 | } 86 | 87 | .clearfix { 88 | &:after { 89 | visibility: hidden; 90 | display: block; 91 | font-size: 0; 92 | content: " "; 93 | clear: both; 94 | height: 0; 95 | } 96 | } 97 | 98 | aside { 99 | background: #eef1f6; 100 | padding: 8px 24px; 101 | margin-bottom: 20px; 102 | border-radius: 2px; 103 | display: block; 104 | line-height: 32px; 105 | font-size: 16px; 106 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 107 | color: #2c3e50; 108 | -webkit-font-smoothing: antialiased; 109 | -moz-osx-font-smoothing: grayscale; 110 | 111 | a { 112 | color: #337ab7; 113 | cursor: pointer; 114 | 115 | &:hover { 116 | color: rgb(32, 160, 255); 117 | } 118 | } 119 | } 120 | 121 | //main-container全局样式 122 | .app-container { 123 | padding: 20px; 124 | } 125 | 126 | .components-container { 127 | margin: 30px 50px; 128 | position: relative; 129 | } 130 | 131 | .pagination-container { 132 | margin-top: 30px; 133 | } 134 | 135 | .text-center { 136 | text-align: center 137 | } 138 | 139 | .sub-navbar { 140 | height: 50px; 141 | line-height: 50px; 142 | position: relative; 143 | width: 100%; 144 | text-align: right; 145 | padding-right: 20px; 146 | transition: 600ms ease position; 147 | background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%); 148 | 149 | .subtitle { 150 | font-size: 20px; 151 | color: #fff; 152 | } 153 | 154 | &.draft { 155 | background: #d0d0d0; 156 | } 157 | 158 | &.deleted { 159 | background: #d0d0d0; 160 | } 161 | } 162 | 163 | .link-type, 164 | .link-type:focus { 165 | color: #337ab7; 166 | cursor: pointer; 167 | 168 | &:hover { 169 | color: rgb(32, 160, 255); 170 | } 171 | } 172 | 173 | .filter-container { 174 | padding-bottom: 10px; 175 | 176 | .filter-item { 177 | display: inline-block; 178 | vertical-align: middle; 179 | margin-bottom: 10px; 180 | } 181 | } 182 | 183 | //refine vue-multiselect plugin 184 | .multiselect { 185 | line-height: 16px; 186 | } 187 | 188 | .el-tooltip__popper { 189 | max-width: 300px; 190 | line-height: 180%; 191 | } 192 | 193 | .multiselect--active { 194 | z-index: 1000 !important; 195 | } 196 | -------------------------------------------------------------------------------- /src/styles/intro.scss: -------------------------------------------------------------------------------- 1 | .customTooltip { 2 | width: 300px; 3 | } 4 | 5 | .customTooltip .text-blue { 6 | color: #0366d6; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | @mixin pct($pct) { 31 | width: #{$pct}; 32 | position: relative; 33 | margin: 0 auto; 34 | } 35 | 36 | @mixin triangle($width, $height, $color, $direction) { 37 | $width: $width/2; 38 | $color-border-style: $height solid $color; 39 | $transparent-border-style: $width solid transparent; 40 | height: 0; 41 | width: 0; 42 | 43 | @if $direction==up { 44 | border-bottom: $color-border-style; 45 | border-left: $transparent-border-style; 46 | border-right: $transparent-border-style; 47 | } 48 | 49 | @else if $direction==right { 50 | border-left: $color-border-style; 51 | border-top: $transparent-border-style; 52 | border-bottom: $transparent-border-style; 53 | } 54 | 55 | @else if $direction==down { 56 | border-top: $color-border-style; 57 | border-left: $transparent-border-style; 58 | border-right: $transparent-border-style; 59 | } 60 | 61 | @else if $direction==left { 62 | border-right: $color-border-style; 63 | border-top: $transparent-border-style; 64 | border-bottom: $transparent-border-style; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/styles/sidebar.scss: -------------------------------------------------------------------------------- 1 | #app { 2 | 3 | .main-container { 4 | min-height: 100%; 5 | transition: margin-left .28s; 6 | margin-left: $sideBarWidth; 7 | position: relative; 8 | } 9 | 10 | .sidebar-container { 11 | transition: width 0.28s; 12 | width: $sideBarWidth !important; 13 | background-color: $menuBg; 14 | height: 100%; 15 | position: fixed; 16 | font-size: 0px; 17 | top: 0; 18 | bottom: 0; 19 | left: 0; 20 | z-index: 1001; 21 | overflow: hidden; 22 | 23 | // reset element-ui css 24 | .horizontal-collapse-transition { 25 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; 26 | } 27 | 28 | .scrollbar-wrapper { 29 | overflow-x: hidden !important; 30 | } 31 | 32 | .el-scrollbar__bar.is-vertical { 33 | right: 0px; 34 | } 35 | 36 | .el-scrollbar { 37 | height: 100%; 38 | } 39 | 40 | &.has-logo { 41 | .el-scrollbar { 42 | height: calc(100% - 50px); 43 | } 44 | } 45 | 46 | .is-horizontal { 47 | display: none; 48 | } 49 | 50 | a { 51 | display: inline-block; 52 | width: 100%; 53 | overflow: hidden; 54 | } 55 | 56 | .svg-icon { 57 | margin-right: 16px; 58 | } 59 | 60 | .sub-el-icon { 61 | margin-right: 12px; 62 | margin-left: -2px; 63 | } 64 | 65 | .el-menu { 66 | border: none; 67 | height: 100%; 68 | width: 100% !important; 69 | } 70 | 71 | // menu hover 72 | .submenu-title-noDropdown, 73 | .el-submenu__title { 74 | &:hover { 75 | background-color: $menuHover !important; 76 | } 77 | } 78 | 79 | .is-active>.el-submenu__title { 80 | color: $subMenuActiveText !important; 81 | } 82 | 83 | & .nest-menu .el-submenu>.el-submenu__title, 84 | & .el-submenu .el-menu-item { 85 | min-width: $sideBarWidth !important; 86 | background-color: $subMenuBg !important; 87 | 88 | &:hover { 89 | background-color: $subMenuHover !important; 90 | } 91 | } 92 | } 93 | 94 | .hideSidebar { 95 | .sidebar-container { 96 | width: 54px !important; 97 | } 98 | 99 | .main-container { 100 | margin-left: 54px; 101 | } 102 | 103 | .submenu-title-noDropdown { 104 | padding: 0 !important; 105 | position: relative; 106 | 107 | .el-tooltip { 108 | padding: 0 !important; 109 | 110 | .svg-icon { 111 | margin-left: 20px; 112 | } 113 | 114 | .sub-el-icon { 115 | margin-left: 19px; 116 | } 117 | } 118 | } 119 | 120 | .el-submenu { 121 | overflow: hidden; 122 | 123 | &>.el-submenu__title { 124 | padding: 0 !important; 125 | 126 | .svg-icon { 127 | margin-left: 20px; 128 | } 129 | 130 | .sub-el-icon { 131 | margin-left: 19px; 132 | } 133 | 134 | .el-submenu__icon-arrow { 135 | display: none; 136 | } 137 | } 138 | } 139 | 140 | .el-menu--collapse { 141 | .el-submenu { 142 | &>.el-submenu__title { 143 | &>span { 144 | height: 0; 145 | width: 0; 146 | overflow: hidden; 147 | visibility: hidden; 148 | display: inline-block; 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | .el-menu--collapse .el-menu .el-submenu { 156 | min-width: $sideBarWidth !important; 157 | } 158 | 159 | // mobile responsive 160 | .mobile { 161 | .main-container { 162 | margin-left: 0px; 163 | } 164 | 165 | .sidebar-container { 166 | transition: transform .28s; 167 | width: $sideBarWidth !important; 168 | } 169 | 170 | &.hideSidebar { 171 | .sidebar-container { 172 | pointer-events: none; 173 | transition-duration: 0.3s; 174 | transform: translate3d(-$sideBarWidth, 0, 0); 175 | } 176 | } 177 | } 178 | 179 | .withoutAnimation { 180 | 181 | .main-container, 182 | .sidebar-container { 183 | transition: none; 184 | } 185 | } 186 | } 187 | 188 | // when menu collapsed 189 | .el-menu--vertical { 190 | &>.el-menu { 191 | .svg-icon { 192 | margin-right: 16px; 193 | } 194 | .sub-el-icon { 195 | margin-right: 12px; 196 | margin-left: -2px; 197 | } 198 | } 199 | 200 | .nest-menu .el-submenu>.el-submenu__title, 201 | .el-menu-item { 202 | &:hover { 203 | // you can use $subMenuHover 204 | background-color: $menuHover !important; 205 | } 206 | } 207 | 208 | // the scroll bar appears when the subMenu is too long 209 | >.el-menu--popup { 210 | max-height: 100vh; 211 | overflow-y: auto; 212 | 213 | &::-webkit-scrollbar-track-piece { 214 | background: #d3dce6; 215 | } 216 | 217 | &::-webkit-scrollbar { 218 | width: 6px; 219 | } 220 | 221 | &::-webkit-scrollbar-thumb { 222 | background: #99a9bf; 223 | border-radius: 20px; 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.2s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .2s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .2s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all .2s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // base color 2 | $blue:#324157; 3 | $light-blue:#3A71A8; 4 | $red:#C03639; 5 | $pink: #E65D6E; 6 | $green: #30B08F; 7 | $tiffany: #4AB7BD; 8 | $yellow:#FEC171; 9 | $panGreen: #30B08F; 10 | 11 | // sidebar 12 | $menuText:#bfcbd9; 13 | $menuActiveText:#409EFF; 14 | $subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951 15 | 16 | $menuBg:#304156; 17 | $menuHover:#263445; 18 | 19 | $subMenuBg:#1f2d3d; 20 | $subMenuHover:#001528; 21 | 22 | $sideBarWidth: 190px; 23 | 24 | // the :export directive is the magic sauce for webpack 25 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 26 | :export { 27 | menuText: $menuText; 28 | menuActiveText: $menuActiveText; 29 | subMenuActiveText: $subMenuActiveText; 30 | menuBg: $menuBg; 31 | menuHover: $menuHover; 32 | subMenuBg: $subMenuBg; 33 | subMenuHover: $subMenuHover; 34 | sideBarWidth: $sideBarWidth; 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | 2 | const TokenKey = 'wecross-token' 3 | const UsernameKey = 'wecross-user' 4 | const PubKey = 'wecross-pub' 5 | 6 | export function getToken() { 7 | return localStorage.getItem(TokenKey) 8 | } 9 | 10 | export function setToken(token) { 11 | return localStorage.setItem(TokenKey, token) 12 | } 13 | 14 | export function removeToken() { 15 | return localStorage.removeItem(TokenKey) 16 | } 17 | 18 | export function getUsername() { 19 | return localStorage.getItem(UsernameKey) 20 | } 21 | 22 | export function setUsername(username) { 23 | return localStorage.setItem(UsernameKey, username) 24 | } 25 | 26 | export function removeUsername() { 27 | return localStorage.removeItem(UsernameKey) 28 | } 29 | 30 | export function isUserFirstTimeUse(username) { 31 | // not first time 32 | if (localStorage.getItem(username)) { 33 | return false 34 | } else { 35 | localStorage.setItem(username, '1') 36 | return true 37 | } 38 | } 39 | 40 | export function getPubKey(pub) { 41 | return localStorage.getItem(PubKey) 42 | } 43 | 44 | export function setPubKey(pub) { 45 | return localStorage.setItem(PubKey, pub) 46 | } 47 | 48 | export function removePubKey() { 49 | return localStorage.removeItem(PubKey) 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/authcode.js: -------------------------------------------------------------------------------- 1 | import { authCode } from '@/api/user' 2 | import { authPub } from '@/api/user' 3 | import { setPubKey } from '@/utils/auth' 4 | import { Message } from 'element-ui' 5 | 6 | /** 7 | * query server encrypt publicKey 8 | */ 9 | export function queryPub(callback) { 10 | authPub().then((resp) => { 11 | if (!resp) { 12 | Message({ 13 | type: 'error', 14 | message: 'response is null, check server status.' 15 | }) 16 | if ( 17 | typeof resp.errorCode !== 'undefined' && 18 | resp.errorCode !== null && 19 | resp.errorCode !== 0 20 | ) { 21 | Message({ 22 | type: 'error', 23 | message: JSON.stringify(resp) 24 | }) 25 | } 26 | } else { 27 | const pub = resp.data.pub 28 | setPubKey(pub) 29 | if (typeof callback !== 'undefined' && callback !== null) { 30 | callback() 31 | } 32 | } 33 | }) 34 | } 35 | 36 | /** 37 | * query auth code 38 | */ 39 | export function queryAuthCode(callback) { 40 | authCode().then((resp) => { 41 | if (!resp) { 42 | Message({ 43 | type: 'error', 44 | message: 'response is null, check server status.' 45 | }) 46 | if ( 47 | typeof resp.errorCode !== 'undefined' && 48 | resp.errorCode !== null && 49 | resp.errorCode !== 0 50 | ) { 51 | Message({ 52 | type: 'error', 53 | message: JSON.stringify(resp) 54 | }) 55 | } 56 | } else { 57 | callback(resp.data.authCode) 58 | } 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /src/utils/chainAccountIntro.js: -------------------------------------------------------------------------------- 1 | import introJS from 'intro.js' 2 | import 'intro.js/introjs.css' 3 | import 'intro.js/themes/introjs-modern.css' 4 | import { listAccount } from '@/api/ua' 5 | import { Message } from 'element-ui' 6 | 7 | /** 8 | * check if chain account fit the chainTypes 9 | * @param {Array|String} chainTypes - the type of chains to query 10 | * @param {Function} callBack - what to do on exiting intro 11 | * @return {IntroJs|null} 12 | */ 13 | export function isChainAccountFit(chainTypes, callBack) { 14 | if (!chainTypes) { 15 | Message.error('Wrong chain types, please check.') 16 | return null 17 | } 18 | listAccount().then(res => { 19 | const chainAccountTypes = new Set() 20 | for (const chainAccount of res.data.chainAccounts) { 21 | chainAccountTypes.add(chainAccount.type) 22 | } 23 | if (chainTypes instanceof Array) { 24 | for (const chainType of chainTypes) { 25 | if (chainAccountTypes.size < 1 || !chainAccountTypes.has(chainType)) { 26 | return introJS().setOptions({ 27 | prevLabel: '上一步', 28 | nextLabel: '下一步', 29 | doneLabel: '结束', 30 | disableInteraction: true, 31 | tooltipClass: 'customTooltip', 32 | steps: [ 33 | { 34 | title: '警告⚠️', 35 | intro: '请检查是否正确配置链账户未配置链账户将会影响基本使用' 36 | }, 37 | { 38 | element: '#Account', 39 | title: '账户管理', 40 | intro: '请在这里配置链账户信息账户功能详情介绍请参考:账号服务', 41 | position: 'right' 42 | } 43 | ] 44 | }).start().onexit(callBack) 45 | } 46 | } 47 | callBack() 48 | } else if (typeof chainTypes === 'string') { 49 | if (!chainAccountTypes.has(chainTypes)) { 50 | console.log(chainAccountTypes, chainTypes) 51 | return introJS().setOptions({ 52 | prevLabel: '上一步', 53 | nextLabel: '下一步', 54 | doneLabel: '结束', 55 | disableInteraction: true, 56 | tooltipClass: 'customTooltip', 57 | steps: [ 58 | { 59 | title: '警告⚠️', 60 | intro: '请检查是否正确配置链账户未配置链账户将会影响基本使用' 61 | }, 62 | { 63 | element: '#Account', 64 | title: '账户管理', 65 | intro: '请在这里配置链账户信息账户功能详情介绍请参考:账号服务', 66 | position: 'right' 67 | } 68 | ] 69 | }).start().onexit(callBack) 70 | } else { 71 | callBack() 72 | } 73 | } 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /src/utils/clipboard.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Clipboard from 'clipboard' 3 | 4 | function clipboardSuccess() { 5 | Vue.prototype.$message({ 6 | message: '复制成功', 7 | type: 'success', 8 | duration: 1500 9 | }) 10 | } 11 | 12 | function clipboardError() { 13 | Vue.prototype.$message({ 14 | message: '复制失败', 15 | type: 'error' 16 | }) 17 | } 18 | 19 | export default function handleClipboard(text, event) { 20 | const clipboard = new Clipboard(event.target, { 21 | text: () => text 22 | }) 23 | clipboard.on('success', () => { 24 | clipboardSuccess() 25 | clipboard.destroy() 26 | }) 27 | clipboard.on('error', () => { 28 | clipboardError() 29 | clipboard.destroy() 30 | }) 31 | clipboard.onClick(event) 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/errorcode.js: -------------------------------------------------------------------------------- 1 | export const ErrorCode = { 2 | Success: 0, 3 | InvalidParameters: 40001, 4 | UAAccountExist: 40002, 5 | UAAccountNotExist: 40003, 6 | ChainAccountExist: 40004, 7 | ChainAccountNotExist: 40005, 8 | ConfigurationItemError: 40006, 9 | AccountOrPasswordIncorrect: 40007, 10 | ImageAuthTokenExpired: 40008, 11 | ImageAuthTokenNotExist: 40009, 12 | ImageAuthTokenNotMatch: 40010, 13 | CreateUAFailed: 40011, 14 | UserHasLogout: 40012, 15 | ChainAccountTypeNotFound: 40013, 16 | FlushDataException: 40014, 17 | UndefinedError: 40099 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | const title = 'WeCross Web App' 2 | 3 | export default function getPageTitle(pageTitle) { 4 | if (pageTitle) { 5 | return `${pageTitle} - ${title}` 6 | } 7 | return `${title}` 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse the time to string 3 | * @param {(Object|string|number)} time 4 | * @param {string|null} cFormat 5 | * @returns {string | null} 6 | */ 7 | export function parseTime(time, cFormat) { 8 | if (arguments.length === 0 || !time) { 9 | return null 10 | } 11 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 12 | let date 13 | if (typeof time === 'object') { 14 | date = time 15 | } else { 16 | if ((typeof time === 'string')) { 17 | if ((/^[0-9]+$/.test(time))) { 18 | // support "1548221490638" 19 | time = parseInt(time) 20 | } else { 21 | // support safari 22 | // https://stackoverflow.com/questions/4310953/invalid-date-in-safari 23 | time = time.replace(new RegExp(/-/gm), '/') 24 | } 25 | } 26 | 27 | if ((typeof time === 'number') && (time.toString().length === 10)) { 28 | time = time * 1000 29 | } 30 | date = new Date(time) 31 | } 32 | const formatObj = { 33 | y: date.getFullYear(), 34 | m: date.getMonth() + 1, 35 | d: date.getDate(), 36 | h: date.getHours(), 37 | i: date.getMinutes(), 38 | s: date.getSeconds(), 39 | a: date.getDay() 40 | } 41 | const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { 42 | const value = formatObj[key] 43 | // Note: getDay() returns 0 on Sunday 44 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } 45 | return value.toString().padStart(2, '0') 46 | }) 47 | return time_str 48 | } 49 | 50 | /** 51 | * @param {number} time 52 | * @param {string} option 53 | * @returns {string} 54 | */ 55 | export function formatTime(time, option) { 56 | if (('' + time).length === 10) { 57 | time = parseInt(time) * 1000 58 | } else { 59 | time = +time 60 | } 61 | const d = new Date(time) 62 | const now = Date.now() 63 | 64 | const diff = (now - d) / 1000 65 | 66 | if (diff < 30) { 67 | return '刚刚' 68 | } else if (diff < 3600) { 69 | // less 1 hour 70 | return Math.ceil(diff / 60) + '分钟前' 71 | } else if (diff < 3600 * 24) { 72 | return Math.ceil(diff / 3600) + '小时前' 73 | } else if (diff < 3600 * 24 * 2) { 74 | return '1天前' 75 | } 76 | if (option) { 77 | return parseTime(time, option) 78 | } else { 79 | return ( 80 | d.getMonth() + 81 | 1 + 82 | '月' + 83 | d.getDate() + 84 | '日' + 85 | d.getHours() + 86 | '时' + 87 | d.getMinutes() + 88 | '分' 89 | ) 90 | } 91 | } 92 | 93 | /** 94 | * @param {string} url 95 | * @returns {Object} 96 | */ 97 | export function param2Obj(url) { 98 | const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') 99 | if (!search) { 100 | return {} 101 | } 102 | const obj = {} 103 | const searchArr = search.split('&') 104 | searchArr.forEach(v => { 105 | const index = v.indexOf('=') 106 | if (index !== -1) { 107 | const name = v.substring(0, index) 108 | obj[name] = v.substring(index + 1, v.length) 109 | } 110 | }) 111 | return obj 112 | } 113 | 114 | export function uniqueFilter(element, index, self) { 115 | return self.indexOf(element) === index 116 | } 117 | 118 | export function uniqueObjectArray(objectArray) { 119 | return [...new Set(objectArray.map(e => JSON.stringify(e)))].map(e => JSON.parse(e)) 120 | } 121 | 122 | export function buildRequest(path, method, data) { 123 | return { 124 | version: 1, 125 | path: path, 126 | method: method, 127 | data: data 128 | } 129 | } 130 | 131 | export function path2Url(path) { 132 | const part = path.split('.') 133 | return '/' + part.join('/') 134 | } 135 | 136 | export function limitString(str, limitNum = 40) { 137 | if (typeof str === 'string') { 138 | if (str.length > limitNum) { 139 | return str.substring(0, limitNum) + '...' 140 | } else { 141 | return str 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/utils/messageBox.js: -------------------------------------------------------------------------------- 1 | import { MessageBox } from 'element-ui' 2 | import Vue from 'vue' 3 | 4 | /** 5 | * handle a long error message output 6 | * @param {string} message - set to msgBox's main body 7 | * @param {string} title - set to msgBox's title 8 | * @param {string} errorMessage - set to textarea, a long output 9 | * @param {Object|null} options - other msgBox options 10 | * @return {Promise} 11 | */ 12 | export function handleErrorMsgBox(message, title, errorMessage, options) { 13 | const vm = new Vue() 14 | const h = vm.$createElement 15 | return MessageBox({ 16 | message: h('div', { style: { 17 | display: 'block', 18 | fontSize: '16px', 19 | fontFamily: 'Helvetica Neue', 20 | width: '100%', 21 | height: '100%' 22 | }}, 23 | [ 24 | h('i', { 25 | style: { 26 | color: '#f56c6c', 27 | marginRight: '6px' 28 | }, 29 | attrs: { 30 | class: 'el-icon-error' 31 | } 32 | }, ''), 33 | h('span', null, message), 34 | h('div', { 35 | style: { display: 'block' }, 36 | attrs: { 37 | } 38 | }, [ 39 | h('textarea', { 40 | attrs: { 41 | readonly: true 42 | }, 43 | style: { 44 | color: '#606266', 45 | margin: '10px 0', 46 | padding: '8px 10px', 47 | height: 'auto', 48 | minHeight: '100px', 49 | maxHeight: '150px', 50 | overflow: 'auto', 51 | width: '100%', 52 | resize: 'none', 53 | fontSize: '15px', 54 | border: '0px' 55 | } 56 | }, errorMessage) 57 | ]) 58 | ]), 59 | title: title, 60 | ...options 61 | }) 62 | } 63 | 64 | /** 65 | * handle a long success message output 66 | * @param {string} message - set to msgBox's main body 67 | * @param {string} title - set to msgBox's title 68 | * @param {string} successMessage - set to textarea, a long output 69 | * @param {Object|null} options - other msgBox options 70 | * @return {Promise} 71 | */ 72 | export function handleSuccessMsgBox(message, title, successMessage, options) { 73 | const vm = new Vue() 74 | const h = vm.$createElement 75 | return MessageBox({ 76 | message: h('div', { style: { 77 | display: 'block', 78 | fontSize: '16px', 79 | fontFamily: 'Helvetica Neue', 80 | width: '100%' 81 | }}, 82 | [ 83 | h('i', { 84 | style: { 85 | color: '#67C23A', 86 | marginRight: '6px' 87 | }, 88 | attrs: { 89 | class: 'el-icon-success' 90 | } 91 | }, ''), 92 | h('span', null, message), 93 | h('div', { 94 | style: { display: 'block' }, 95 | attrs: { 96 | } 97 | }, [ 98 | h('textarea', { 99 | attrs: { 100 | readonly: true 101 | }, 102 | style: { 103 | color: '#606266', 104 | margin: '10px 0', 105 | padding: '8px 10px', 106 | height: 'auto', 107 | minHeight: '80px', 108 | maxHeight: '150px', 109 | overflow: 'auto', 110 | width: '100%', 111 | resize: 'none', 112 | fontSize: '15px', 113 | border: '0px' 114 | } 115 | }, successMessage) 116 | ]) 117 | ]), 118 | title: title, 119 | ...options 120 | }) 121 | } 122 | 123 | /** 124 | * handle a long warning message output 125 | * @param {string} message - set to msgBox's main body 126 | * @param {string} title - set to msgBox's title 127 | * @param {string} warningMessage - set to textarea, a long output 128 | * @param {Object|null} options - other msgBox options 129 | * @return {Promise} 130 | */ 131 | export function handleWarningMsgBox(message, title, warningMessage, options) { 132 | const vm = new Vue() 133 | const h = vm.$createElement 134 | return MessageBox({ 135 | message: h('div', { style: { 136 | display: 'block', 137 | fontSize: '16px', 138 | fontFamily: 'Helvetica Neue', 139 | width: '100%', 140 | height: '100%' 141 | }}, 142 | [ 143 | h('i', { 144 | style: { 145 | color: '#e6a23c', 146 | marginRight: '6px' 147 | }, 148 | attrs: { 149 | class: 'el-icon-warning' 150 | } 151 | }, ''), 152 | h('span', null, message), 153 | h('div', { 154 | style: { display: 'block' }, 155 | attrs: { 156 | } 157 | }, [ 158 | h('textarea', { 159 | attrs: { 160 | readonly: true 161 | }, 162 | style: { 163 | color: '#606266', 164 | margin: '10px 0', 165 | padding: '8px 10px', 166 | height: 'auto', 167 | minHeight: '100px', 168 | maxHeight: '150px', 169 | overflow: 'auto', 170 | width: '100%', 171 | resize: 'none', 172 | fontSize: '15px', 173 | border: '0px' 174 | } 175 | }, warningMessage) 176 | ]) 177 | ]), 178 | title: title, 179 | ...options 180 | }) 181 | } 182 | -------------------------------------------------------------------------------- /src/utils/pem.js: -------------------------------------------------------------------------------- 1 | import { ec as EC } from 'elliptic' 2 | import { keccak256 } from 'js-sha3' 3 | import { sm2 as SM2 } from 'sm-crypto' 4 | import { sm3Hex } from '@/utils/sm3.js' 5 | 6 | const CERT_PATTERN = 7 | '-----BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+' + // Header 8 | '([A-Za-z0-9+/=\\r\\n]+)' + // Base64 text 9 | '-----END\\s+.*CERTIFICATE[^-]*-+' // Footer 10 | 11 | const SEC_KEY_PATTERN = 12 | '-----BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+' + // Header 13 | '([A-Za-z0-9+/=\\r\\n]+)' + // Base64 text 14 | '-----END\\s+.*PRIVATE\\s+KEY[^-]*-+' // Footer 15 | const PUB_KEY_PATTERN = 16 | '-----BEGIN\\s+.*PUBLIC\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+' + // Header 17 | '([A-Za-z0-9+/=\\r\\n]+)' + // Base64 text 18 | '-----END\\s+.*PUBLIC\\s+KEY[^-]*-+' // Footer 19 | 20 | export const pem = { 21 | isCertFormat(content) { 22 | return content.search(CERT_PATTERN) === 0 23 | }, 24 | 25 | isSecKeyFormat(content) { 26 | return content.search(SEC_KEY_PATTERN) === 0 27 | }, 28 | 29 | isPubKeyFormat(content) { 30 | return content.search(PUB_KEY_PATTERN) === 0 31 | } 32 | } 33 | 34 | // ECDSA 35 | 36 | const ecdsaSecPemPrefix = '308184020100301006072a8648ce3d020106052b8104000a046d306b0201010420' 37 | const ecdsaPubPemPrefix = '3056301006072a8648ce3d020106052b8104000a034200' 38 | 39 | const ecdsaSecPemFingerprint = '020100301006072a8648ce3d020106052b8104000a' 40 | 41 | function getPubKeyHexFromECDSASecPem(secKeyContent) { 42 | var base64Content = secKeyContent.substr(0, secKeyContent.lastIndexOf('-----END')).replace('\n', '').replace('-----BEGIN PRIVATE KEY-----', '') 43 | 44 | var buffer = Buffer.from(base64Content, 'base64') 45 | var hexString = buffer.toString('hex') 46 | var pubKeyHex = hexString.substr(hexString.length - 130, 130) 47 | return pubKeyHex 48 | } 49 | 50 | function buildECDSAPubKeyPem(pubKeyHex) { 51 | var asn1HexString = ecdsaPubPemPrefix + pubKeyHex 52 | var base64String = Buffer.from(asn1HexString, 'hex').toString('base64') 53 | 54 | return '-----BEGIN PUBLIC KEY-----\n' + base64String + '\n-----END PUBLIC KEY-----\n' 55 | } 56 | 57 | function ecdsaPub2Addr(pubKeyHex) { 58 | var pubKeyHexWithoutPrefix = pubKeyHex.substr(2, 128) // No prefix 04 59 | var address = '0x' + keccak256(Uint8Array.from(Buffer.from(pubKeyHexWithoutPrefix, 'hex'))).substr(24, 40) 60 | return address 61 | } 62 | 63 | export const ecdsa = { 64 | generateSecPem() { 65 | const secp256k1 = new EC('secp256k1') 66 | var keyPair = secp256k1.genKeyPair() 67 | 68 | var pubKey = keyPair.getPublic('hex') 69 | var secKey = keyPair.getPrivate('hex') 70 | 71 | var asn1HexString = ecdsaSecPemPrefix + secKey + 'a144034200' + pubKey 72 | var base64String = Buffer.from(asn1HexString, 'hex').toString('base64') 73 | 74 | return '-----BEGIN PRIVATE KEY-----\n' + base64String + '\n-----END PRIVATE KEY-----\n' 75 | }, 76 | isSecPem(secPem) { 77 | if (!pem.isSecKeyFormat(secPem)) { 78 | return false 79 | } 80 | 81 | var base64Content = secPem.replace('\n', '').replace('-----BEGIN PRIVATE KEY-----', '') 82 | 83 | var buffer = Buffer.from(base64Content, 'base64') 84 | var hexString = buffer.toString('hex') 85 | 86 | return hexString.includes(ecdsaSecPemFingerprint) 87 | }, 88 | build(secPem) { 89 | var pubKeyHex = getPubKeyHexFromECDSASecPem(secPem) 90 | 91 | return { 92 | secPem: secPem, 93 | pubPem: buildECDSAPubKeyPem(pubKeyHex), 94 | address: ecdsaPub2Addr(pubKeyHex) 95 | } 96 | } 97 | } 98 | 99 | // SM2 100 | 101 | const sm2SecPemPrefix = '308187020100301306072a8648ce3d020106082a811ccf5501822d046d306b0201010420' 102 | const sm2PubPemPrefix = '3059301306072a8648ce3d020106082a811ccf5501822d034200' 103 | 104 | const sm2SecPemFingerprint = '020100301306072a8648ce3d020106082a811ccf5501822d04' 105 | 106 | function getPubKeyHexFromSM2SecPem(secKeyContent) { 107 | var base64Content = secKeyContent.substr(0, secKeyContent.lastIndexOf('-----END')).replace('\n', '').replace('-----BEGIN PRIVATE KEY-----', '') 108 | 109 | var buffer = Buffer.from(base64Content, 'base64') 110 | var hexString = buffer.toString('hex') 111 | var pubKeyHex = hexString.substr(hexString.length - 130, 130) 112 | return pubKeyHex 113 | } 114 | 115 | function buildSM2PubKeyPem(pubKeyHex) { 116 | var asn1HexString = sm2PubPemPrefix + pubKeyHex 117 | var base64String = Buffer.from(asn1HexString, 'hex').toString('base64') 118 | 119 | return '-----BEGIN PUBLIC KEY-----\n' + base64String + '\n-----END PUBLIC KEY-----\n' 120 | } 121 | 122 | function sm2Pub2Addr(pubKeyHex) { 123 | var pubKeyHexWithoutPrefix = pubKeyHex.substr(2, 128) 124 | 125 | var address = '0x' + sm3Hex(pubKeyHexWithoutPrefix).substr(24, 40) 126 | return address 127 | } 128 | 129 | export const sm2 = { 130 | generateSecPem() { 131 | const keyPair = SM2.generateKeyPairHex() 132 | 133 | var asn1HexString = sm2SecPemPrefix + keyPair.privateKey + 'a144034200' + keyPair.publicKey 134 | var base64String = Buffer.from(asn1HexString, 'hex').toString('base64') 135 | 136 | return '-----BEGIN PRIVATE KEY-----\n' + base64String + '\n-----END PRIVATE KEY-----\n' 137 | }, 138 | isSecPem(secPem) { 139 | if (!pem.isSecKeyFormat(secPem)) { 140 | return false 141 | } 142 | 143 | var base64Content = secPem.replace('\n', '').replace('-----BEGIN PRIVATE KEY-----', '') 144 | 145 | var buffer = Buffer.from(base64Content, 'base64') 146 | var hexString = buffer.toString('hex') 147 | 148 | return hexString.includes(sm2SecPemFingerprint) 149 | }, 150 | build(secPem) { 151 | var pubKeyHex = getPubKeyHexFromSM2SecPem(secPem) 152 | 153 | return { 154 | secPem: secPem, 155 | pubPem: buildSM2PubKeyPem(pubKeyHex), 156 | address: sm2Pub2Addr(pubKeyHex) 157 | } 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { MessageBox, Message } from 'element-ui' 3 | import store from '@/store' 4 | import { getToken } from '@/utils/auth' 5 | 6 | // create an axios instance 7 | const request = axios.create({ 8 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url 9 | // withCredentials: true, // send cookies when cross-domain requests 10 | timeout: 30000 // request timeout 11 | }) 12 | 13 | // request interceptor 14 | request.interceptors.request.use( 15 | config => { 16 | // do something before request is sent 17 | 18 | if (store.getters.token) { 19 | if (store.getters.token !== getToken()) { 20 | return Promise.reject(new Error('needRefresh')) 21 | } 22 | config.headers['Authorization'] = getToken() 23 | config.headers['Accept'] = 'application/json' 24 | config.headers['content-type'] = 'application/json;charset=UTF-8' 25 | } 26 | return config 27 | }, 28 | error => { 29 | // do something with request error 30 | console.log('err: ', error) // for debug 31 | return Promise.reject(error) 32 | } 33 | ) 34 | 35 | // response interceptor 36 | request.interceptors.response.use( 37 | response => { 38 | if (!response) { 39 | Message({ 40 | message: 'HTTP返回为空!', 41 | type: 'error', 42 | duration: 5 * 1000 43 | }) 44 | return Promise.reject(new Error('HTTP返回为空')) 45 | } 46 | if (!response.status) { 47 | Message({ 48 | message: 'HTTP响应状态码为空!', 49 | type: 'error', 50 | duration: 5 * 1000 51 | }) 52 | return Promise.reject(new Error('HTTP响应状态码为空!')) 53 | } 54 | const res = response.data 55 | const status = response.status 56 | if (status !== 200) { 57 | Message({ 58 | message: res.message || 'Error: status is' + status, 59 | type: 'error', 60 | duration: 5 * 1000 61 | }) 62 | 63 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; 64 | if (res.errorCode === 10502 || res.errorCode === 50012 || res.errorCode === 50014) { 65 | // to re-login 66 | MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', { 67 | confirmButtonText: 'Re-Login', 68 | cancelButtonText: 'Cancel', 69 | type: 'warning' 70 | }).then(() => { 71 | store.dispatch('user/resetToken').then(() => { 72 | location.reload() 73 | }) 74 | }) 75 | } 76 | return Promise.reject(new Error(res.message || 'Error')) 77 | } else { 78 | return res 79 | } 80 | }, 81 | error => { 82 | // another tab login, should fresh status 83 | if (axios.isCancel(error)) { 84 | throw new Error('canceled') 85 | } else if (error.message === 'needRefresh') { 86 | throw new Error('当前页面状态已变化,请刷新页面再重试!') 87 | } 88 | // timeout 89 | if (error.message.includes('timeout')) { 90 | Message({ 91 | message: '请求超时,请检查后台服务:' + error.message, 92 | type: 'error', 93 | duration: 5 * 1000 94 | }) 95 | return Promise.reject(new Error(error.message || '请求超时,请检查后台服务')) 96 | } 97 | // no response status 98 | if (typeof error.response !== 'undefined' && typeof error.response.status !== 'undefined') { 99 | const status = error.response.status 100 | switch (status) { 101 | case 401 : 102 | MessageBox.confirm('您的登录态已超时,请重新登录', '超时提醒', { 103 | confirmButtonText: '重登录', 104 | cancelButtonText: '取消', 105 | type: 'warning' 106 | }).then(() => { 107 | store.dispatch('user/resetToken').then(() => { 108 | location.reload() 109 | }) 110 | }) 111 | break 112 | case 400: 113 | Message.error({ 114 | message: '参数异常' 115 | }) 116 | break 117 | case 404: 118 | Message.error({ 119 | message: '请求URL错误:' + error.message, 120 | center: true, 121 | duration: 5 * 1000 122 | }) 123 | break 124 | case 500: 125 | Message.error({ 126 | message: '服务器异常' 127 | }) 128 | break 129 | default: 130 | Message({ 131 | message: error.message, 132 | type: 'error', 133 | duration: 5 * 1000 134 | }) 135 | } 136 | console.log('err: ' + error) // for debug 137 | return Promise.reject(error) 138 | } 139 | } 140 | ) 141 | 142 | export default request 143 | -------------------------------------------------------------------------------- /src/utils/resource.js: -------------------------------------------------------------------------------- 1 | export function clearForm(formData) { 2 | formData.className = null 3 | formData.version = null 4 | formData.address = null 5 | formData.org = null 6 | formData.lang = null 7 | formData.policy = 'default' 8 | formData.args = null 9 | formData.chosenSolidity = null 10 | formData.sourceContent = null 11 | formData.compressedContent = null 12 | } 13 | 14 | export function buildBCOSDeployRequest(formData) { 15 | return { 16 | version: 1, 17 | path: formData.fullPath || formData.prependPath + formData.appendPath, 18 | data: { 19 | command: formData.method, 20 | args: [formData.appendPath || formData.fullPath.split('.')[2], formData.sourceContent, formData.className, formData.version] 21 | } 22 | } 23 | } 24 | 25 | export function buildBCOSDeployWasmRequest(formData) { 26 | return { 27 | version: 1, 28 | path: formData.fullPath || formData.prependPath + formData.appendPath, 29 | data: { 30 | command: formData.method, 31 | args: [formData.appendPath || formData.fullPath.split('.')[2], formData.abiContent, formData.sourceContent] 32 | } 33 | } 34 | } 35 | 36 | export function buildBCOSRegisterRequest(formData) { 37 | return { 38 | version: 1, 39 | path: formData.fullPath || formData.prependPath + formData.appendPath, 40 | data: { 41 | command: formData.method, 42 | args: [formData.appendPath || formData.fullPath.split('.')[2], formData.chosenSolidity.split('.')[1], formData.sourceContent, '0x' + formData.address, formData.className, formData.version] 43 | } 44 | } 45 | } 46 | 47 | export function buildFabricInstallRequest(formData) { 48 | return { 49 | version: 1, 50 | path: formData.fullPath || formData.prependPath + formData.appendPath, 51 | data: { 52 | command: formData.method, 53 | args: [ 54 | formData.appendPath || formData.fullPath.split('.')[2], 55 | formData.version, 56 | formData.org.trim(), 57 | formData.lang, 58 | formData.compressedContent 59 | ] 60 | } 61 | } 62 | } 63 | 64 | export function buildFabricInstantiateRequest(formData) { 65 | return { 66 | version: 1, 67 | path: formData.fullPath || formData.prependPath + formData.appendPath, 68 | data: { 69 | command: formData.method, 70 | args: [ 71 | formData.appendPath || formData.fullPath.split('.')[2], 72 | formData.version, 73 | formData.org, 74 | formData.lang, 75 | formData.policy === 'default' ? '' : formData.policy, 76 | formData.args.trim() 77 | ] 78 | } 79 | } 80 | } 81 | 82 | export function buildFabricUpgradeRequest(formData) { 83 | return { 84 | version: 1, 85 | path: formData.fullPath || formData.prependPath + formData.appendPath, 86 | data: { 87 | command: formData.method, 88 | args: [ 89 | formData.appendPath || formData.fullPath.split('.')[2], 90 | formData.version, 91 | formData.org, 92 | formData.lang, 93 | formData.policy === 'default' ? '' : formData.policy, 94 | formData.args 95 | ] 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/utils/rsa.js: -------------------------------------------------------------------------------- 1 | import JSEncrypt from 'jsencrypt' 2 | 3 | /** 4 | * rsa encrypt 5 | */ 6 | export function rsa_encode(input, pub) { 7 | const encryptor = new JSEncrypt(null) 8 | encryptor.setPublicKey(pub) 9 | return encryptor.encrypt(input) 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/transaction.js: -------------------------------------------------------------------------------- 1 | const xaTransactionKey = 'xaTX' 2 | 3 | /** 4 | * @typedef {Object} xaTransaction 5 | * @property {string} transactionID 6 | * @property {Array} paths 7 | */ 8 | 9 | /** 10 | * get XA transaction in sessionStorage 11 | * @return {xaTransaction|null} 12 | */ 13 | export function getXATX() { 14 | return JSON.parse(sessionStorage.getItem(xaTransactionKey)) 15 | } 16 | 17 | export function setXATX(xaTX) { 18 | return sessionStorage.setItem(xaTransactionKey, xaTX) 19 | } 20 | 21 | export function removeXATX() { 22 | return sessionStorage.removeItem(xaTransactionKey) 23 | } 24 | 25 | export function buildXAResponseError(response) { 26 | if (typeof response.data !== 'undefined' && response.data !== null && 27 | typeof response.data.xaResponse.chainErrorMessages !== 'undefined' && 28 | response.data.xaResponse.chainErrorMessages !== []) { 29 | let str = '' 30 | for (const chainErrorMessage of response.data.xaResponse.chainErrorMessages) { 31 | str += chainErrorMessage.path + ': ' + chainErrorMessage.message + '\n\n' 32 | } 33 | return str 34 | } else { 35 | return response.message 36 | } 37 | } 38 | 39 | export function buildXAError(response) { 40 | if (typeof response.data !== 'undefined' && response.data !== null && 41 | typeof response.data.chainErrorMessages !== 'undefined' && 42 | response.data.chainErrorMessages !== []) { 43 | let str = '' 44 | for (const chainErrorMessage of response.data.chainErrorMessages) { 45 | str += chainErrorMessage.path + ': ' + chainErrorMessage.message + '\n\n' 46 | } 47 | return str 48 | } else { 49 | return response.message 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | import { sha256 } from 'js-sha256' 2 | 3 | /** 4 | * @param {string} path 5 | * @returns {Boolean} 6 | */ 7 | export function isExternal(path) { 8 | return /^(https?:|mailto:|tel:)/.test(path) 9 | } 10 | 11 | /** 12 | * @param {string} str 13 | * @returns {Boolean} 14 | */ 15 | export function validUsername(str) { 16 | return /^[a-zA-Z0-9_-]{3,18}$/.test(str) 17 | } 18 | 19 | /** 20 | * @param {string} str 21 | * @returns {Boolean} 22 | */ 23 | export function validPassword(str) { 24 | // return /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,18}$/.test(str) 25 | return /^(?=.*([a-zA-Z].*))(?=.*[0-9].*)[a-zA-Z0-9-*/+.~!@#$%^&*()_-]{6,18}$/.test(str) 26 | } 27 | 28 | /** 29 | * 30 | * @param {*} str 31 | */ 32 | export function confusePassword(str) { 33 | var constantSalt = '1234567890~!#$%^&*()_+' 34 | var passwdWithSalt = constantSalt + str 35 | return sha256(passwdWithSalt) 36 | } 37 | 38 | /** 39 | * @param {string} url 40 | * @returns {Boolean} 41 | */ 42 | export function validURL(url) { 43 | const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ 44 | return reg.test(url) 45 | } 46 | 47 | /** 48 | * @param {string} str 49 | * @returns {Boolean} 50 | */ 51 | export function validLowerCase(str) { 52 | const reg = /^[a-z]+$/ 53 | return reg.test(str) 54 | } 55 | 56 | /** 57 | * @param {string} str 58 | * @returns {Boolean} 59 | */ 60 | export function validUpperCase(str) { 61 | const reg = /^[A-Z]+$/ 62 | return reg.test(str) 63 | } 64 | 65 | /** 66 | * @param {string} str 67 | * @returns {Boolean} 68 | */ 69 | export function validAlphabets(str) { 70 | const reg = /^[A-Za-z]+$/ 71 | return reg.test(str) 72 | } 73 | 74 | /** 75 | * @param {string} str 76 | * @returns {Boolean} 77 | */ 78 | export function isString(str) { 79 | return typeof str === 'string' || str instanceof String 80 | } 81 | 82 | /** 83 | * @param {Array} arg 84 | * @returns {Boolean} 85 | */ 86 | export function isArray(arg) { 87 | if (typeof Array.isArray === 'undefined') { 88 | return Object.prototype.toString.call(arg) === '[object Array]' 89 | } 90 | return Array.isArray(arg) 91 | } 92 | 93 | /** 94 | * @param {string} path 95 | * @return {Boolean} 96 | */ 97 | export function isValidPath(path) { 98 | const reg = /^[A-Za-z]+\.[A-Za-z]+\.[A-Za-z]+$/ 99 | return reg.test(path) 100 | } 101 | -------------------------------------------------------------------------------- /src/views/document/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Welcome to WeCross 5 | 6 | 7 | 8 | WeCross是由微众银行自主研发并完全开源的区块链跨链协作平台,致力于促进跨行业、机构和地域的跨区块链信任传递和商业合作。 9 | WeCross不局限于满足同构区块链平行扩展后的可信数据交换需求,还进一步探索解决异构区块链之间因底层架构、 10 | 数据结构、接口协议、安全机制等多维异构性导致无法互联互通问题的有效方案。 11 | 12 | 13 | 14 | 15 | WeCross 在线文档 16 | 17 | WeCross GitHub 18 | 19 | WeCross 20 | WeCross-Java-SDK 21 | WeCross-Console 22 | WeCross-WebApp 23 | 24 | 25 | WeCross-Fabric1-Stub 26 | WeCross-BCOS2-Stub 27 | WeCross-Account-Manager 28 | 29 | 30 | 31 | 32 | 44 | 45 | 82 | -------------------------------------------------------------------------------- /src/views/resource/resourceManager.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 导航 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 资源列表 23 | 24 | 25 | 26 | {if(typeof currentChain !== 'undefined'){$refs['ResourceExplorer'].refresh()}}" /> 27 | 28 | 29 | 部署资源 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 103 | 104 | 107 | -------------------------------------------------------------------------------- /src/views/resource/resourceSteps/resourceDeploySteps.js: -------------------------------------------------------------------------------- 1 | 2 | export function stepRoute(type, method) { 3 | if (type === null) { 4 | return defaultSteps 5 | } 6 | if (type === 'BCOS2.0' || type === 'GM_BCOS2.0') { 7 | return BCOSSteps 8 | } else if (type === 'Fabric1.4') { 9 | if (method === null) { 10 | return [{ 11 | element: '#method', 12 | title: '选择操作', 13 | intro: '请先选择操作类型', 14 | position: 'bottom' 15 | } 16 | ] 17 | } 18 | if (method === 'install') { 19 | return FabricInstallSteps 20 | } else { 21 | return FabricInstantSteps 22 | } 23 | } 24 | } 25 | 26 | const BCOSSteps = [ 27 | { 28 | element: '#method', 29 | title: '1. 选择操作', 30 | intro: '选择操作类型', 31 | position: 'top' 32 | }, { 33 | element: '#Path', 34 | title: '2. 资源路径', 35 | intro: '填写资源路径', 36 | position: 'top' 37 | }, { 38 | element: '#zipContract', 39 | title: '3. 选取合约上传', 40 | intro: '选取需要部署的合约文件,打包成ZIP格式上传打包zip文件时,合约入口必须放在最外层合约的所有依赖文件必须按照依赖相对路径一起打包', 41 | position: 'right' 42 | }, { 43 | element: '#chosenSolidity', 44 | title: '4. 选取合约入口文件', 45 | intro: '页面会读取您所上传的zip文件的最外层,选择入口文件进行部署', 46 | position: 'left' 47 | }, { 48 | element: '#className', 49 | title: '5. 填写合约类名', 50 | intro: '合约类名是您选取的"合约入口文件"中的合约类名', 51 | position: 'top' 52 | }, { 53 | element: '#bcosVersion', 54 | title: '6. 填写合约版本号', 55 | intro: '若在链中已有待部署合约的旧版本,填写较大版本可对旧合约进行升级', 56 | position: 'top' 57 | }, { 58 | element: '#onSubmit', 59 | title: '执行部署操作', 60 | intro: '检查表单,执行操作', 61 | position: 'top' 62 | } 63 | ] 64 | 65 | const FabricInstallSteps = [ 66 | { 67 | element: '#method', 68 | title: '1. 选择操作', 69 | intro: '选择操作类型', 70 | position: 'top' 71 | }, { 72 | element: '#Path', 73 | title: '2. 资源路径', 74 | intro: '填写资源路径', 75 | position: 'top' 76 | }, { 77 | element: '#org', 78 | title: '3. 所属机构名', 79 | intro: '安装链码的endorser所属的机构名当前账户必须有对应机构的证书密钥才可以正确安装chaincode', 80 | position: 'top' 81 | }, { 82 | element: '#compressedContent', 83 | title: '4. 选择合约文件', 84 | intro: '选取需要部署的chaincode合约文件,打包成GZIP格式上传打包时必须将链码放在"src/chaincode/"的目录下在能正确安装文件路径例如:src/chaincode/sacc.go', 85 | position: 'top' 86 | }, { 87 | element: '#fabricVersion', 88 | title: '5. 填写合约版本号', 89 | intro: '若在链中已有待部署合约的旧版本,填写较大版本可对旧合约进行升级', 90 | position: 'top' 91 | }, { 92 | element: '#lang', 93 | title: '6. 选择合约语言版本', 94 | intro: 'Golang/Java', 95 | position: 'top' 96 | }, { 97 | element: '#onSubmit', 98 | title: '执行部署操作', 99 | intro: '检查表单,执行操作因为Fabric的原因,安装的chaincode必须实例化才能显示在跨链资源列表', 100 | position: 'top' 101 | } 102 | ] 103 | 104 | const FabricInstantSteps = [ 105 | { 106 | element: '#method', 107 | title: '1. 选择操作', 108 | intro: '选择操作类型', 109 | position: 'top' 110 | }, { 111 | element: '#Path', 112 | title: '2. 资源路径', 113 | intro: '填写资源路径', 114 | position: 'top' 115 | }, { 116 | element: '#orgs', 117 | title: '3. 机构列表', 118 | intro: '链码被安装的的机构列表将chaincode在列表中的机构进行实例化必须以JSON数组的形式填写,例如:["Org1","Org2"]', 119 | position: 'top' 120 | }, { 121 | element: '#fabricVersion', 122 | title: '4. 填写合约版本号', 123 | intro: '选择对应版本的chaincode进行实例化', 124 | position: 'top' 125 | }, { 126 | element: '#lang', 127 | title: '5. 选择合约语言版本', 128 | intro: 'Golang/Java', 129 | position: 'top' 130 | }, { 131 | element: '#policy', 132 | title: '6. 选择背书文件', 133 | intro: '选择实例化时的背书只能上传policy的yaml格式文件, 不上传默认为defaultdefault为所有机构以OR连接', 134 | position: 'top' 135 | }, { 136 | element: '#args', 137 | title: '7. 填写实例化参数', 138 | intro: '填写实例化时的参数必须以JSON数组的形式填写,例如:["a","10"],若参数为空也必须填写 []', 139 | position: 'top' 140 | }, { 141 | element: '#onSubmit', 142 | title: '执行部署操作', 143 | intro: '检查表单,执行操作', 144 | position: 'top' 145 | } 146 | ] 147 | 148 | const defaultSteps = [ 149 | { 150 | element: '#stubType', 151 | title: '1. 选择链类型', 152 | intro: '选择链的类型', 153 | position: 'bottom' 154 | }, { 155 | element: '#method', 156 | title: '2. 选择操作', 157 | intro: '选择操作类型', 158 | position: 'top' 159 | } 160 | ] 161 | -------------------------------------------------------------------------------- /src/views/resource/resourceSteps/resourceManagerSteps.js: -------------------------------------------------------------------------------- 1 | export const resourceManagerSteps = [ 2 | { 3 | element: '#ChainExplorer', 4 | title: '1. 导航选择', 5 | intro: '第一步:从zone-chain导航中选择对应的链', 6 | position: 'right' 7 | }, 8 | { 9 | element: '#ResourceExplorer', 10 | title: '2. 资源列表展示', 11 | intro: '第二步:选择好对应的链之后,就会在资源列表中展示这条链中所有资源', 12 | position: 'left' 13 | }, 14 | { 15 | element: '#resourceDeploy', 16 | title: '资源部署', 17 | intro: '若需要部署资源,请点击"部署资源"按钮', 18 | position: 'left' 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /src/views/router/routerGuide.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 31 | 32 | 35 | -------------------------------------------------------------------------------- /src/views/router/routerManager.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 刷新 8 | 添加跨链路由 9 | 10 | 11 | 12 | 13 | 14 | 15 | {{ getAlias(item.row.nodeID) !== null ? getAlias(item.row.nodeID) : '未设置' }} 16 | 17 | 18 | 19 | 20 | 21 | {{ item.row.nodeID==='Local'?'':item.row.nodeID }} 22 | {{ item.row.nodeID }} 23 | 24 | 25 | 26 | 27 | {{ item.row.address }} 28 | 29 | 30 | 31 | 32 | 33 | {{ chainItem.stubType }} 34 | 35 | 36 | 37 | 38 | 39 | 40 | 设置别名 41 | 42 | 43 | 44 | 45 | 46 | 47 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 154 | 155 | 158 | -------------------------------------------------------------------------------- /src/views/transaction/transactionManager.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 导航 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 交易列表 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 发交易 31 | 32 | 33 | 34 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /src/views/transaction/transactionSteps/transactionManagerSteps.js: -------------------------------------------------------------------------------- 1 | export const transactionManagerSteps = [ 2 | { 3 | element: '#ChainExplorer', 4 | title: '1. 导航选择', 5 | intro: '第一步:从zone-chain导航中选择对应的链', 6 | position: 'right' 7 | }, 8 | { 9 | element: '#TransactionListExplorer', 10 | title: '2. 交易列表展示', 11 | intro: '第二步:选择好对应的链之后,就会在资源列表中展示这条链中所有交易', 12 | position: 'left' 13 | }, 14 | { 15 | element: '#sendTransaction', 16 | title: '发起交易', 17 | intro: '若需要发起跨链交易,请点击"发交易"按钮', 18 | position: 'left' 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /src/views/transaction/transactionSteps/xaTransactionStep.js: -------------------------------------------------------------------------------- 1 | export const xaTransactionManagerSteps = [ 2 | { 3 | element: '#ChainExplorer', 4 | title: '1. 导航选择', 5 | intro: '第一步:从zone-chain导航中选择对应的链', 6 | position: 'right' 7 | }, 8 | { 9 | element: '#xaTransactionListExplorer', 10 | title: '2. 事务列表展示', 11 | intro: '第二步:选择好对应的链之后,就会在资源列表中展示这条链中所有事务', 12 | position: 'left' 13 | }, 14 | { 15 | element: '#sendxaTransaction', 16 | title: '发起交易', 17 | intro: '若需要发起事务,请点击"发起事务"按钮', 18 | position: 'left' 19 | } 20 | ] 21 | 22 | export const startXASteps = [ 23 | { 24 | element: '#XAID', 25 | title: '开启事务', 26 | intro: '第一步:生成事务ID!只支持输入16进制', 27 | position: 'bottom' 28 | }, 29 | { 30 | element: '#XAPath', 31 | title: '开启事务', 32 | intro: '第二步:选择事务资源!从左边待选资源列表勾选资源,点击添加按钮到已选资源列表', 33 | position: 'top' 34 | }, 35 | { 36 | element: '#btnGroup', 37 | title: '开启事务', 38 | intro: '第三步:点击开启事务!', 39 | position: 'top' 40 | } 41 | ] 42 | 43 | export const execXASteps = [ 44 | { 45 | element: '#xaForm', 46 | title: '执行事务', 47 | intro: '执行事务交易!填写资源、方法和参数,执行调用', 48 | position: 'top' 49 | }, 50 | { 51 | element: '#xaList', 52 | title: '执行事务', 53 | intro: '查看事务步骤!查看事务详细步骤', 54 | position: 'top' 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } -------------------------------------------------------------------------------- /tests/unit/utils/formatTime.spec.js: -------------------------------------------------------------------------------- 1 | import { formatTime } from '@/utils/index.js' 2 | describe('Utils:formatTime', () => { 3 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 4 | const retrofit = 5 * 1000 5 | 6 | it('ten digits timestamp', () => { 7 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') 8 | }) 9 | it('test now', () => { 10 | expect(formatTime(+new Date() - 1)).toBe('刚刚') 11 | }) 12 | it('less two minute', () => { 13 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') 14 | }) 15 | it('less two hour', () => { 16 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') 17 | }) 18 | it('less one day', () => { 19 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') 20 | }) 21 | it('more than one day', () => { 22 | expect(formatTime(d)).toBe('7月13日17时54分') 23 | }) 24 | it('format', () => { 25 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 26 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 27 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /tests/unit/utils/param2Obj.spec.js: -------------------------------------------------------------------------------- 1 | import { param2Obj } from '@/utils/index.js' 2 | describe('Utils:param2Obj', () => { 3 | const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95' 4 | 5 | it('param2Obj test', () => { 6 | expect(param2Obj(url)).toEqual({ 7 | name: 'bill', 8 | age: '29', 9 | sex: '1', 10 | field: window.btoa('test'), 11 | key: '测试' 12 | }) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /tests/unit/utils/parseTime.spec.js: -------------------------------------------------------------------------------- 1 | import { parseTime } from '@/utils/index.js' 2 | 3 | describe('Utils:parseTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | it('timestamp', () => { 6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01') 7 | }) 8 | 9 | it('timestamp string', () => { 10 | expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01') 11 | }) 12 | 13 | it('ten digits timestamp', () => { 14 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') 15 | }) 16 | it('new Date', () => { 17 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') 18 | }) 19 | it('format', () => { 20 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 21 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 22 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 23 | }) 24 | it('get the day of the week', () => { 25 | expect(parseTime(d, '{a}')).toBe('五') // 星期五 26 | }) 27 | it('get the day of the week', () => { 28 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 29 | }) 30 | it('empty argument', () => { 31 | expect(parseTime()).toBeNull() 32 | }) 33 | 34 | it('null', () => { 35 | expect(parseTime(null)).toBeNull() 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /tests/unit/utils/validate.spec.js: -------------------------------------------------------------------------------- 1 | import { validUsername, validURL, validLowerCase, validUpperCase, validAlphabets, isString } from '@/utils/validate.js' 2 | import { path2Url } from '@/utils' 3 | describe('Utils:validate', () => { 4 | it('validUsername', () => { 5 | expect(validUsername('admin1')).toBe(true) 6 | expect(validUsername('org-user1')).toBe(true) 7 | expect(validUsername('xx')).toBe(false) 8 | }) 9 | it('validURL', () => { 10 | expect(validURL('https://github.com/WeBankBlockchain/WeCross-WebApp.git')).toBe(true) 11 | expect(validURL('http://github.com/WeBankBlockchain/WeCross-WebApp.git')).toBe(true) 12 | expect(validURL('github.com/WeBankBlockchain/WeCross-WebApp.git')).toBe(false) 13 | }) 14 | it('validLowerCase', () => { 15 | expect(validLowerCase('abc')).toBe(true) 16 | expect(validLowerCase('Abc')).toBe(false) 17 | expect(validLowerCase('123abc')).toBe(false) 18 | }) 19 | it('validUpperCase', () => { 20 | expect(validUpperCase('ABC')).toBe(true) 21 | expect(validUpperCase('Abc')).toBe(false) 22 | expect(validUpperCase('123ABC')).toBe(false) 23 | }) 24 | it('validAlphabets', () => { 25 | expect(validAlphabets('ABC')).toBe(true) 26 | expect(validAlphabets('Abc')).toBe(true) 27 | expect(validAlphabets('123aBC')).toBe(false) 28 | }) 29 | it('isString', () => { 30 | expect(isString('')).toBe(true) 31 | const obj = { 32 | str: 'str' 33 | } 34 | expect(isString(obj.str)).toBe(true) 35 | expect(isString(obj)).toBe(false) 36 | expect(isString(123)).toBe(false) 37 | }) 38 | it('path2URL', () => { 39 | expect(path2Url('test.test.test')).toBe('/test/test/test') 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | 4 | function resolve(dir) { 5 | return path.join(__dirname, dir) 6 | } 7 | 8 | const name = 'WeCross Web App' // page title 9 | 10 | // If your port is set to 80, 11 | // use administrator privileges to execute the command line. 12 | // For example, Mac: sudo npm run 13 | // You can change the port by the following methods: 14 | // port = 9528 npm run dev OR npm run dev --port = 9528 15 | const port = process.env.port || process.env.npm_config_port || 9528 // dev port 16 | 17 | // All configuration item explanations can be find in https://cli.vuejs.org/config/ 18 | module.exports = { 19 | /** 20 | * You will need to set publicPath if you plan to deploy your site under a sub path, 21 | * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/, 22 | * then publicPath should be set to "/bar/". 23 | * In most cases please use '/' !!! 24 | * Detail: https://cli.vuejs.org/config/#publicpath 25 | */ 26 | publicPath: './', 27 | outputDir: 'dist', 28 | assetsDir: 'static', 29 | lintOnSave: process.env.NODE_ENV === 'development', 30 | productionSourceMap: false, 31 | devServer: { 32 | port: port, 33 | host: '0.0.0.0', 34 | open: true, 35 | overlay: { 36 | warnings: false, 37 | errors: true 38 | }, 39 | proxy: 'http://121.37.203.43:8250/' 40 | // before: require('./mock/mock-server.js') 41 | }, 42 | configureWebpack: { 43 | // provide the app's title in webpack's name field, so that 44 | // it can be accessed in index.html to inject the correct title. 45 | name: name, 46 | resolve: { 47 | alias: { 48 | '@': resolve('src') 49 | } 50 | } 51 | }, 52 | chainWebpack(config) { 53 | // it can improve the speed of the first screen, it is recommended to turn on preload 54 | config.plugin('preload').tap(() => [ 55 | { 56 | rel: 'preload', 57 | // to ignore runtime.js 58 | // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171 59 | fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/], 60 | include: 'initial' 61 | } 62 | ]) 63 | 64 | // when there are many pages, it will cause too many meaningless requests 65 | config.plugins.delete('prefetch') 66 | 67 | // set svg-sprite-loader 68 | config.module 69 | .rule('svg') 70 | .exclude.add(resolve('src/icons')) 71 | .end() 72 | config.module 73 | .rule('icons') 74 | .test(/\.svg$/) 75 | .include.add(resolve('src/icons')) 76 | .end() 77 | .use('svg-sprite-loader') 78 | .loader('svg-sprite-loader') 79 | .options({ 80 | symbolId: 'icon-[name]' 81 | }) 82 | .end() 83 | 84 | config 85 | .when(process.env.NODE_ENV !== 'development', 86 | subConfig => { 87 | subConfig 88 | .plugin('ScriptExtHtmlWebpackPlugin') 89 | .after('html') 90 | .use('script-ext-html-webpack-plugin', [{ 91 | // `runtime` must same as runtimeChunk name. default is `runtime` 92 | inline: /runtime\..*\.js$/ 93 | }]) 94 | .end() 95 | subConfig 96 | .optimization.splitChunks({ 97 | chunks: 'all', 98 | cacheGroups: { 99 | libs: { 100 | name: 'chunk-libs', 101 | test: /[\\/]node_modules[\\/]/, 102 | priority: 10, 103 | chunks: 'initial' // only package third parties that are initially dependent 104 | }, 105 | elementUI: { 106 | name: 'chunk-elementUI', // split elementUI into a single package 107 | priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app 108 | test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm 109 | }, 110 | commons: { 111 | name: 'chunk-commons', 112 | test: resolve('src/components'), // can customize your rules 113 | minChunks: 3, // minimum common number 114 | priority: 5, 115 | reuseExistingChunk: true 116 | } 117 | } 118 | }) 119 | // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk 120 | subConfig.optimization.runtimeChunk('single') 121 | } 122 | ) 123 | } 124 | } 125 | --------------------------------------------------------------------------------
选取需要部署的合约文件,打包成ZIP格式上传
选取需要部署的chaincode合约文件,打包成GZIP格式上传
选择实例化时的背书