diff --git a/fit2cloud-view/PENDING.md b/fit2cloud-view/PENDING.md new file mode 100644 index 0000000000..32dbb2e0a8 --- /dev/null +++ b/fit2cloud-view/PENDING.md @@ -0,0 +1,17 @@ +# 功能 + +- [x] 登录页面 +- [x] 整体布局 +- [x] 路由基础框架 +- [x] 左侧菜单 +- [x] API基础框架 +- [x] mock +- [x] 国际化及规范 +- [x] 加载FIT2CLOUD UI +- [ ] 权限控制 +- [ ] 完整Demo路由及页面 + - [ ] 权限 Demo + - [ ] Form Demo + - [ ] Table Demo +- [ ] 说明文档 + diff --git a/fit2cloud-view/README.md b/fit2cloud-view/README.md new file mode 100644 index 0000000000..6952e9355b --- /dev/null +++ b/fit2cloud-view/README.md @@ -0,0 +1 @@ +# FIT2CLOUD 应用模板 diff --git a/fit2cloud-view/babel.config.js b/fit2cloud-view/babel.config.js new file mode 100644 index 0000000000..e9558405fd --- /dev/null +++ b/fit2cloud-view/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + '@vue/cli-plugin-babel/preset' + ] +} diff --git a/fit2cloud-view/mock/index.js b/fit2cloud-view/mock/index.js new file mode 100644 index 0000000000..d8d14e4a0f --- /dev/null +++ b/fit2cloud-view/mock/index.js @@ -0,0 +1,54 @@ +const Mock = require('mockjs') +const {param2Obj} = require('./utils') + +const user = require('./user') + +const mocks = [ + ...user, +] + +// for front mock +// please use it cautiously, it will redefine XMLHttpRequest, +// which will cause many of your third-party libraries to be invalidated(like progress event). +function mockXHR() { + // mock patch + // https://github.com/nuysoft/Mock/issues/300 + Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send + Mock.XHR.prototype.send = function () { + if (this.custom.xhr) { + this.custom.xhr.withCredentials = this.withCredentials || false + + if (this.responseType) { + this.custom.xhr.responseType = this.responseType + } + } + this.proxy_send(...arguments) + } + + function XHR2ExpressReqWrap(respond) { + return function (options) { + let result; + if (respond instanceof Function) { + const {body, type, url} = options + // https://expressjs.com/en/4x/api.html#req + result = respond({ + method: type, + body: JSON.parse(body), + query: param2Obj(url) + }) + } else { + result = respond + } + return Mock.mock(result) + } + } + + for (const i of mocks) { + Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) + } +} + +module.exports = { + mocks, + mockXHR +} diff --git a/fit2cloud-view/mock/license.js b/fit2cloud-view/mock/license.js new file mode 100644 index 0000000000..1c9e3fe157 --- /dev/null +++ b/fit2cloud-view/mock/license.js @@ -0,0 +1,51 @@ +const {success, error} = require("./result-holder") + +const licenses = { + valid: { + status: "valid", + license: { + "corporation": "xxxxxxxxxxxx", + "expired": "2030-07-03", + "licenseVersion": "v2", + "product": "cmp", + "generateTime": "1593763389356", + "edition": "Enterprise", + "count": 11 + }, + message: "" + }, + invalid: { + status: "invalid", + license: {}, + message: "license has invalid" + }, + expired: { + status: "expired", + license: { + "corporation": "xxxxxxxxxxxx", + "expired": "2020-07-03", + "licenseVersion": "v2", + "product": "cmp", + "generateTime": "1593763389356", + "edition": "Enterprise", + "count": 11 + }, + message: "license has expired since 2020-07-03" + }, +} + +module.exports = [ + { + url: '/samples/license/save', + type: 'post', + response: config => { + const {license} = config.body + const data = licenses[license]; + + if (!data) { + return success(licenses.invalid) + } + return success(data) + } + }, +] diff --git a/fit2cloud-view/mock/mock-server.js b/fit2cloud-view/mock/mock-server.js new file mode 100644 index 0000000000..8941ec0f80 --- /dev/null +++ b/fit2cloud-view/mock/mock-server.js @@ -0,0 +1,81 @@ +const chokidar = require('chokidar') +const bodyParser = require('body-parser') +const chalk = require('chalk') +const path = require('path') +const Mock = require('mockjs') + +const mockDir = path.join(process.cwd(), 'mock') + +function registerRoutes(app) { + let mockLastIndex + const { mocks } = require('./index.js') + const mocksForServer = mocks.map(route => { + return responseFake(route.url, route.type, route.response) + }) + for (const mock of mocksForServer) { + app[mock.type](mock.url, mock.response) + mockLastIndex = app._router.stack.length + } + const mockRoutesLength = Object.keys(mocksForServer).length + return { + mockRoutesLength: mockRoutesLength, + mockStartIndex: mockLastIndex - mockRoutesLength + } +} + +function unregisterRoutes() { + Object.keys(require.cache).forEach(i => { + if (i.includes(mockDir)) { + delete require.cache[require.resolve(i)] + } + }) +} + +// for mock server +const responseFake = (url, type, respond) => { + return { + url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`), + type: type || 'get', + response(req, res) { + console.log('request invoke:' + req.path) + res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) + } + } +} + +module.exports = app => { + // parse app.body + // https://expressjs.com/en/4x/api.html#req.body + app.use(bodyParser.json()) + app.use(bodyParser.urlencoded({ + extended: true + })) + + const mockRoutes = registerRoutes(app) + var mockRoutesLength = mockRoutes.mockRoutesLength + var mockStartIndex = mockRoutes.mockStartIndex + + // watch files, hot reload mock server + chokidar.watch(mockDir, { + ignored: /mock-server/, + ignoreInitial: true + }).on('all', (event, path) => { + if (event === 'change' || event === 'add') { + try { + // remove mock routes stack + app._router.stack.splice(mockStartIndex, mockRoutesLength) + + // clear routes cache + unregisterRoutes() + + const mockRoutes = registerRoutes(app) + mockRoutesLength = mockRoutes.mockRoutesLength + mockStartIndex = mockRoutes.mockStartIndex + + console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`)) + } catch (error) { + console.log(chalk.redBright(error)) + } + } + }) +} diff --git a/fit2cloud-view/mock/result-holder.js b/fit2cloud-view/mock/result-holder.js new file mode 100644 index 0000000000..66db69d87b --- /dev/null +++ b/fit2cloud-view/mock/result-holder.js @@ -0,0 +1,20 @@ +class ResultHolder { + constructor(success, data, message) { + this.success = success; + this.data = data; + this.message = message; + } +} + +const success = data => { + return new ResultHolder(true, data) +} + +const error = message => { + return new ResultHolder(false, undefined, message) +} + +module.exports = { + success, + error +} diff --git a/fit2cloud-view/mock/user-token.js b/fit2cloud-view/mock/user-token.js new file mode 100644 index 0000000000..5fb072ea5a --- /dev/null +++ b/fit2cloud-view/mock/user-token.js @@ -0,0 +1,97 @@ +const {success, error} = require("./result-holder") +const TOKEN_KEY = "App-Token" + +/* 前后端分离,用Token验证登录*/ +const tokens = { + admin: { + token: 'admin-token' + }, + editor: { + token: 'editor-token' + }, + readonly: { + token: 'readonly-token' + } +} + +const users = { + 'admin-token': { + id: "admin", + name: 'Administrator', + email: "admin@fit2cloud.com", + roles: ['admin'], + language: "zh-CN" + }, + 'editor-token': { + id: "editor", + name: 'Editor', + email: "editor@fit2cloud.com", + roles: ['editor'], + language: "zh-CN" + }, + 'readonly-token': { + id: "readonly", + name: 'Readonly User', + email: "readonly@fit2cloud.com", + roles: ['readonly'], + language: "zh-CN" + } +} + +module.exports = [ + // user login + { + url: '/samples/user-token/login', + type: 'post', + response: config => { + const {username} = config.body + const {token} = tokens[username]; + + // mock error + if (!token) { + return error("用户名或密码错误") + } + return success(token) + } + }, + + // get user info + { + url: '/samples/user-token/info', + type: 'get', + response: (config) => { + let token = config.headers[TOKEN_KEY] + const info = users[token] + + // mock error + if (!info) { + return error("无法获取用户[" + token + "]详细信息") + } + + return success(info) + } + }, + + // update user info + { + url: '/samples/user/info/update', + type: 'put', + response: config => { + let token = config.headers[TOKEN_KEY] + const {language} = config.body + users[token].language = language; + + return success(users[token]) + } + }, + + // user logout + { + url: '/samples/user/logout', + type: 'post', + response: () => { + // do something + return success() + } + } +] diff --git a/fit2cloud-view/mock/user.js b/fit2cloud-view/mock/user.js new file mode 100644 index 0000000000..98de3df117 --- /dev/null +++ b/fit2cloud-view/mock/user.js @@ -0,0 +1,98 @@ +const {success, error} = require("./result-holder") + +/* 前后端不分离的接口,用Session验证登录*/ +let currentUser + +const users = { + admin: { + id: "admin", + name: 'Administrator', + email: "admin@fit2cloud.com", + roles: ['admin'], + language: "zh-CN" + }, + editor: { + id: "editor", + name: 'Editor', + email: "editor@fit2cloud.com", + roles: ['editor'], + language: "zh-CN" + }, + readonly: { + id: "readonly", + name: 'Readonly User', + email: "readonly@fit2cloud.com", + roles: ['readonly'], + language: "zh-CN" + } +} + +module.exports = [ + // user login + { + url: '/samples/user/login', + type: 'post', + response: config => { + const {username} = config.body + const user = users[username]; + + // mock error + if (!user) { + return error("用户名或密码错误") + } + currentUser = user; + return success(user) + } + }, + + { + url: '/samples/user/is-login', + type: 'get', + response: () => { + if (currentUser) { + return success() + } else { + return error() + } + } + }, + + // get user info + { + url: '/samples/user/current', + type: 'get', + response: () => { + const info = currentUser + + // mock error + if (!info) { + return error("用户未登录") + } + + return success(info) + } + }, + + // update user info + { + url: '/samples/user/info/update', + type: 'put', + response: config => { + const {language} = config.body + if (currentUser) { + currentUser.language = language; + } + return success(currentUser) + } + }, + + // user logout + { + url: '/samples/user/logout', + type: 'post', + response: () => { + currentUser = undefined; + return success() + } + } +] diff --git a/fit2cloud-view/mock/utils.js b/fit2cloud-view/mock/utils.js new file mode 100644 index 0000000000..f909a29362 --- /dev/null +++ b/fit2cloud-view/mock/utils.js @@ -0,0 +1,48 @@ +/** + * @param {string} url + * @returns {Object} + */ +function param2Obj(url) { + const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') + if (!search) { + return {} + } + const obj = {} + const searchArr = search.split('&') + searchArr.forEach(v => { + const index = v.indexOf('=') + if (index !== -1) { + const name = v.substring(0, index) + const val = v.substring(index + 1, v.length) + obj[name] = val + } + }) + return obj +} + +/** + * This is just a simple version of deep copy + * Has a lot of edge cases bug + * If you want to use a perfect deep copy, use lodash's _.cloneDeep + * @param {Object} source + * @returns {Object} + */ +function deepClone(source) { + if (!source && typeof source !== 'object') { + throw new Error('error arguments', 'deepClone') + } + const targetObj = source.constructor === Array ? [] : {} + Object.keys(source).forEach(keys => { + if (source[keys] && typeof source[keys] === 'object') { + targetObj[keys] = deepClone(source[keys]) + } else { + targetObj[keys] = source[keys] + } + }) + return targetObj +} + +module.exports = { + param2Obj, + deepClone +} diff --git a/fit2cloud-view/package.json b/fit2cloud-view/package.json new file mode 100644 index 0000000000..5f74afdb2a --- /dev/null +++ b/fit2cloud-view/package.json @@ -0,0 +1,58 @@ +{ + "name": "samples", + "version": "0.1.0", + "private": true, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "vue-cli-service lint" + }, + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^1.2.34", + "@fortawesome/free-brands-svg-icons": "^5.15.2", + "@fortawesome/free-regular-svg-icons": "^5.15.2", + "@fortawesome/free-solid-svg-icons": "^5.15.2", + "@fortawesome/vue-fontawesome": "^2.0.2", + "axios": "^0.21.1", + "core-js": "^3.6.5", + "element-ui": "^2.15.0", + "fit2cloud-ui": "^0.1.2", + "js-cookie": "^2.2.1", + "normalize.css": "^8.0.1", + "nprogress": "^0.2.0", + "vue": "^2.6.11", + "vue-i18n": "^8.22.4", + "vuex": "^3.6.0" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "~4.5.0", + "@vue/cli-plugin-eslint": "~4.5.0", + "@vue/cli-service": "~4.5.0", + "babel-eslint": "^10.1.0", + "eslint": "^6.7.2", + "eslint-plugin-vue": "^6.2.2", + "mockjs": "^1.1.0", + "sass": "^1.32.5", + "sass-loader": "^10.1.1", + "vue-template-compiler": "^2.6.11" + }, + "eslintConfig": { + "root": true, + "env": { + "node": true + }, + "extends": [ + "plugin:vue/essential", + "eslint:recommended" + ], + "parserOptions": { + "parser": "babel-eslint" + }, + "rules": {} + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not dead" + ] +} diff --git a/fit2cloud-view/public/favicon.ico b/fit2cloud-view/public/favicon.ico new file mode 100644 index 0000000000..df36fcfb72 Binary files /dev/null and b/fit2cloud-view/public/favicon.ico differ diff --git a/fit2cloud-view/public/index.html b/fit2cloud-view/public/index.html new file mode 100644 index 0000000000..3e5a139621 --- /dev/null +++ b/fit2cloud-view/public/index.html @@ -0,0 +1,17 @@ + + + + + + + + <%= htmlWebpackPlugin.options.title %> + + + +
+ + + diff --git a/fit2cloud-view/src/App.vue b/fit2cloud-view/src/App.vue new file mode 100644 index 0000000000..fb5baad926 --- /dev/null +++ b/fit2cloud-view/src/App.vue @@ -0,0 +1,12 @@ + + + diff --git a/fit2cloud-view/src/api/license.js b/fit2cloud-view/src/api/license.js new file mode 100644 index 0000000000..e9c2ee19bf --- /dev/null +++ b/fit2cloud-view/src/api/license.js @@ -0,0 +1,7 @@ +import {post} from "@/plugins/request" + +export function saveLicense(data) { + return post("/samples/license/save", data) +} + + diff --git a/fit2cloud-view/src/api/user-token.js b/fit2cloud-view/src/api/user-token.js new file mode 100644 index 0000000000..ba12b7d64b --- /dev/null +++ b/fit2cloud-view/src/api/user-token.js @@ -0,0 +1,21 @@ +/* 前后端分离的登录方式 */ +import {get, post, put} from "@/plugins/request" + +export function login(data) { + return post("/samples/user-token/login", data) +} + +export function logout() { + return post("/samples/user-token/logout") +} + +export function getCurrentUser() { + return get("/samples/user-token/current") +} + +export function updateInfo(data) { + return put("/samples/user-token/update", data) +} + + + diff --git a/fit2cloud-view/src/api/user.js b/fit2cloud-view/src/api/user.js new file mode 100644 index 0000000000..bdb9daa82e --- /dev/null +++ b/fit2cloud-view/src/api/user.js @@ -0,0 +1,25 @@ +/* 前后端不分离的登录方式 */ +import {get, post, put} from "@/plugins/request" + +export function login(data) { + return post("/samples/user/login", data) +} + +export function logout() { + return post("/samples/user/logout") +} + +export function isLogin() { + return get("/samples/user/is-login") +} + +export function getCurrentUser() { + return get("/samples/user/current") +} + +export function updateInfo(id, data) { + return put("/samples/user/info/update/" + id, data) +} + + + diff --git a/fit2cloud-view/src/assets/RackShift-assist-white.png b/fit2cloud-view/src/assets/RackShift-assist-white.png new file mode 100644 index 0000000000..b2e556678d Binary files /dev/null and b/fit2cloud-view/src/assets/RackShift-assist-white.png differ diff --git a/fit2cloud-view/src/assets/RackShift-black.png b/fit2cloud-view/src/assets/RackShift-black.png new file mode 100644 index 0000000000..4ccf482ea3 Binary files /dev/null and b/fit2cloud-view/src/assets/RackShift-black.png differ diff --git a/fit2cloud-view/src/assets/RackShift-white.png b/fit2cloud-view/src/assets/RackShift-white.png new file mode 100644 index 0000000000..ca850ba03c Binary files /dev/null and b/fit2cloud-view/src/assets/RackShift-white.png differ diff --git a/fit2cloud-view/src/assets/font/Roboto/Roboto-Regular.ttf b/fit2cloud-view/src/assets/font/Roboto/Roboto-Regular.ttf new file mode 100755 index 0000000000..2b6392ffe8 Binary files /dev/null and b/fit2cloud-view/src/assets/font/Roboto/Roboto-Regular.ttf differ diff --git a/fit2cloud-view/src/assets/font/Roboto/index.css b/fit2cloud-view/src/assets/font/Roboto/index.css new file mode 100644 index 0000000000..b4a21ae2ca --- /dev/null +++ b/fit2cloud-view/src/assets/font/Roboto/index.css @@ -0,0 +1,57 @@ +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-display: swap; + src: url(Roboto-Regular.ttf) format('truetype'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-display: swap; + src: url(Roboto-Regular.ttf) format('truetype'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-display: swap; + src: url(Roboto-Regular.ttf) format('truetype'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-display: swap; + src: url(Roboto-Regular.ttf) format('truetype'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-display: swap; + src: url(Roboto-Regular.ttf) format('truetype'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-display: swap; + src: url(Roboto-Regular.ttf) format('truetype'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-display: swap; + src: url(Roboto-Regular.ttf) format('truetype'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + diff --git a/fit2cloud-view/src/assets/login-desc.png b/fit2cloud-view/src/assets/login-desc.png new file mode 100644 index 0000000000..5d7d73fddc Binary files /dev/null and b/fit2cloud-view/src/assets/login-desc.png differ diff --git a/fit2cloud-view/src/business/app-layout/header-components/LanguageSwitch.vue b/fit2cloud-view/src/business/app-layout/header-components/LanguageSwitch.vue new file mode 100644 index 0000000000..895f480758 --- /dev/null +++ b/fit2cloud-view/src/business/app-layout/header-components/LanguageSwitch.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/fit2cloud-view/src/business/app-layout/header-components/PersonalSetting.vue b/fit2cloud-view/src/business/app-layout/header-components/PersonalSetting.vue new file mode 100644 index 0000000000..baaaeb9be7 --- /dev/null +++ b/fit2cloud-view/src/business/app-layout/header-components/PersonalSetting.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/fit2cloud-view/src/business/app-layout/horizontal-layout/HorizontalHeader.vue b/fit2cloud-view/src/business/app-layout/horizontal-layout/HorizontalHeader.vue new file mode 100644 index 0000000000..a0285eb501 --- /dev/null +++ b/fit2cloud-view/src/business/app-layout/horizontal-layout/HorizontalHeader.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/fit2cloud-view/src/business/app-layout/horizontal-layout/index.vue b/fit2cloud-view/src/business/app-layout/horizontal-layout/index.vue new file mode 100644 index 0000000000..0cb8711f16 --- /dev/null +++ b/fit2cloud-view/src/business/app-layout/horizontal-layout/index.vue @@ -0,0 +1,18 @@ + + + diff --git a/fit2cloud-view/src/business/dashboard/index.vue b/fit2cloud-view/src/business/dashboard/index.vue new file mode 100644 index 0000000000..30353888ad --- /dev/null +++ b/fit2cloud-view/src/business/dashboard/index.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/fit2cloud-view/src/business/directive/ClickOutsideDemo.vue b/fit2cloud-view/src/business/directive/ClickOutsideDemo.vue new file mode 100644 index 0000000000..d4a3c923d9 --- /dev/null +++ b/fit2cloud-view/src/business/directive/ClickOutsideDemo.vue @@ -0,0 +1,13 @@ + + + + + diff --git a/fit2cloud-view/src/business/directive/PermissionDemo.vue b/fit2cloud-view/src/business/directive/PermissionDemo.vue new file mode 100644 index 0000000000..91a5af6686 --- /dev/null +++ b/fit2cloud-view/src/business/directive/PermissionDemo.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/fit2cloud-view/src/business/login/index.vue b/fit2cloud-view/src/business/login/index.vue new file mode 100644 index 0000000000..c05263e7a4 --- /dev/null +++ b/fit2cloud-view/src/business/login/index.vue @@ -0,0 +1,239 @@ + + + + + diff --git a/fit2cloud-view/src/business/system-setting/ParamsSetting.vue b/fit2cloud-view/src/business/system-setting/ParamsSetting.vue new file mode 100644 index 0000000000..0102128649 --- /dev/null +++ b/fit2cloud-view/src/business/system-setting/ParamsSetting.vue @@ -0,0 +1,13 @@ + + + + + diff --git a/fit2cloud-view/src/business/system-setting/UserManagement.vue b/fit2cloud-view/src/business/system-setting/UserManagement.vue new file mode 100644 index 0000000000..76b6d45477 --- /dev/null +++ b/fit2cloud-view/src/business/system-setting/UserManagement.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/fit2cloud-view/src/components/dynamic-table/TablePagination.vue b/fit2cloud-view/src/components/dynamic-table/TablePagination.vue new file mode 100644 index 0000000000..97b515f7c4 --- /dev/null +++ b/fit2cloud-view/src/components/dynamic-table/TablePagination.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/fit2cloud-view/src/components/dynamic-table/index.vue b/fit2cloud-view/src/components/dynamic-table/index.vue new file mode 100644 index 0000000000..f0bebdd428 --- /dev/null +++ b/fit2cloud-view/src/components/dynamic-table/index.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/fit2cloud-view/src/components/layout/LayoutContent.vue b/fit2cloud-view/src/components/layout/LayoutContent.vue new file mode 100644 index 0000000000..223b6457f3 --- /dev/null +++ b/fit2cloud-view/src/components/layout/LayoutContent.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/fit2cloud-view/src/components/layout/LayoutHeader.vue b/fit2cloud-view/src/components/layout/LayoutHeader.vue new file mode 100644 index 0000000000..a293b00e39 --- /dev/null +++ b/fit2cloud-view/src/components/layout/LayoutHeader.vue @@ -0,0 +1,11 @@ + + + diff --git a/fit2cloud-view/src/components/layout/LayoutMain.vue b/fit2cloud-view/src/components/layout/LayoutMain.vue new file mode 100644 index 0000000000..3daee70f64 --- /dev/null +++ b/fit2cloud-view/src/components/layout/LayoutMain.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/fit2cloud-view/src/components/layout/LayoutSidebar.vue b/fit2cloud-view/src/components/layout/LayoutSidebar.vue new file mode 100644 index 0000000000..fbe700ebab --- /dev/null +++ b/fit2cloud-view/src/components/layout/LayoutSidebar.vue @@ -0,0 +1,25 @@ + + + diff --git a/fit2cloud-view/src/components/layout/LayoutView.vue b/fit2cloud-view/src/components/layout/LayoutView.vue new file mode 100644 index 0000000000..f26f429bfc --- /dev/null +++ b/fit2cloud-view/src/components/layout/LayoutView.vue @@ -0,0 +1,20 @@ + + + diff --git a/fit2cloud-view/src/components/layout/index.vue b/fit2cloud-view/src/components/layout/index.vue new file mode 100644 index 0000000000..7f1dc0f938 --- /dev/null +++ b/fit2cloud-view/src/components/layout/index.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/fit2cloud-view/src/components/layout/sidebar/FixiOSBug.js b/fit2cloud-view/src/components/layout/sidebar/FixiOSBug.js new file mode 100644 index 0000000000..bc14856f07 --- /dev/null +++ b/fit2cloud-view/src/components/layout/sidebar/FixiOSBug.js @@ -0,0 +1,26 @@ +export default { + computed: { + device() { + return this.$store.state.app.device + } + }, + mounted() { + // In order to fix the click on menu on the ios device will trigger the mouseleave bug + // https://github.com/PanJiaChen/vue-element-admin/issues/1135 + this.fixBugIniOS() + }, + methods: { + fixBugIniOS() { + const $subMenu = this.$refs.subMenu + if ($subMenu) { + const handleMouseleave = $subMenu.handleMouseleave + $subMenu.handleMouseleave = (e) => { + if (this.device === 'mobile') { + return + } + handleMouseleave(e) + } + } + } + } +} diff --git a/fit2cloud-view/src/components/layout/sidebar/Item.vue b/fit2cloud-view/src/components/layout/sidebar/Item.vue new file mode 100644 index 0000000000..dd6f843c8b --- /dev/null +++ b/fit2cloud-view/src/components/layout/sidebar/Item.vue @@ -0,0 +1,37 @@ + + + diff --git a/fit2cloud-view/src/components/layout/sidebar/Link.vue b/fit2cloud-view/src/components/layout/sidebar/Link.vue new file mode 100644 index 0000000000..af0105262c --- /dev/null +++ b/fit2cloud-view/src/components/layout/sidebar/Link.vue @@ -0,0 +1,43 @@ + + + diff --git a/fit2cloud-view/src/components/layout/sidebar/Logo.vue b/fit2cloud-view/src/components/layout/sidebar/Logo.vue new file mode 100644 index 0000000000..30ff27639a --- /dev/null +++ b/fit2cloud-view/src/components/layout/sidebar/Logo.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/fit2cloud-view/src/components/layout/sidebar/SidebarItem.vue b/fit2cloud-view/src/components/layout/sidebar/SidebarItem.vue new file mode 100644 index 0000000000..05048b48bb --- /dev/null +++ b/fit2cloud-view/src/components/layout/sidebar/SidebarItem.vue @@ -0,0 +1,97 @@ + + + diff --git a/fit2cloud-view/src/components/layout/sidebar/SidebarToggleButton.vue b/fit2cloud-view/src/components/layout/sidebar/SidebarToggleButton.vue new file mode 100644 index 0000000000..bac0fe948f --- /dev/null +++ b/fit2cloud-view/src/components/layout/sidebar/SidebarToggleButton.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/fit2cloud-view/src/components/layout/sidebar/index.vue b/fit2cloud-view/src/components/layout/sidebar/index.vue new file mode 100644 index 0000000000..630b8d0bab --- /dev/null +++ b/fit2cloud-view/src/components/layout/sidebar/index.vue @@ -0,0 +1,266 @@ + + + + + diff --git a/fit2cloud-view/src/components/redirect/index.vue b/fit2cloud-view/src/components/redirect/index.vue new file mode 100644 index 0000000000..db4c1d66d1 --- /dev/null +++ b/fit2cloud-view/src/components/redirect/index.vue @@ -0,0 +1,12 @@ + diff --git a/fit2cloud-view/src/directive/click-outside/index.js b/fit2cloud-view/src/directive/click-outside/index.js new file mode 100644 index 0000000000..7e208289c1 --- /dev/null +++ b/fit2cloud-view/src/directive/click-outside/index.js @@ -0,0 +1,8 @@ +import ClickOutside from "element-ui/src/utils/clickoutside"; + +const install = function (Vue) { + Vue.directive("click-outside", ClickOutside) +} + +ClickOutside.install = install +export default ClickOutside diff --git a/fit2cloud-view/src/directive/index.js b/fit2cloud-view/src/directive/index.js new file mode 100644 index 0000000000..53625ad63d --- /dev/null +++ b/fit2cloud-view/src/directive/index.js @@ -0,0 +1,11 @@ +import ClickOutside from "element-ui/src/utils/clickoutside"; +import permission from "@/directive/permission"; + +export default { + install(Vue) { + Vue.directive('click-outside', ClickOutside); + Vue.directive('permission', permission); + } +} + + diff --git a/fit2cloud-view/src/directive/permission/index.js b/fit2cloud-view/src/directive/permission/index.js new file mode 100644 index 0000000000..d59c22de7e --- /dev/null +++ b/fit2cloud-view/src/directive/permission/index.js @@ -0,0 +1,29 @@ +import store from '@/store' + +function checkPermission(el, binding) { + const {value} = binding + const roles = store.getters && store.getters.roles + + if (value && value instanceof Array) { + if (value.length > 0) { + const permissionRoles = value + + const hasPermission = roles.some(role => { + return permissionRoles.includes(role) + }) + + if (!hasPermission) { + el.parentNode && el.parentNode.removeChild(el) + } + } + } +} + +export default { + inserted(el, binding) { + checkPermission(el, binding) + }, + update(el, binding) { + checkPermission(el, binding) + } +} diff --git a/fit2cloud-view/src/i18n/index.js b/fit2cloud-view/src/i18n/index.js new file mode 100644 index 0000000000..92dd02619f --- /dev/null +++ b/fit2cloud-view/src/i18n/index.js @@ -0,0 +1,75 @@ +import Vue from 'vue'; +import VueI18n from "vue-i18n"; + +Vue.use(VueI18n); + +// 直接加载翻译的语言文件 +const LOADED_LANGUAGES = ['zh-CN', 'en-US']; +const LANG_FILES = require.context('./lang', true, /\.js$/) +// 自动加载lang目录下语言文件,默认只加载LOADED_LANGUAGES中规定的语言文件,其他的语言动态加载 +const messages = LANG_FILES.keys().reduce((messages, path) => { + const value = LANG_FILES(path) + const lang = path.replace(/^\.\/(.*)\.\w+$/, '$1'); + if (LOADED_LANGUAGES.includes(lang)) { + messages[lang] = value.default + } + return messages; +}, {}) + +export const getLanguage = () => { + let language = localStorage.getItem('language') + if (!language) { + language = (navigator.language || navigator.browserLanguage).toLowerCase() + } + return language; +} + +const i18n = new VueI18n({ + locale: getLanguage(), + messages, +}); + +const importLanguage = lang => { + if (!LOADED_LANGUAGES.includes(lang)) { + return import(`./lang/${lang}`).then(response => { + i18n.mergeLocaleMessage(lang, response.default); + LOADED_LANGUAGES.push(lang); + return Promise.resolve(lang) + }) + } + return Promise.resolve(lang) +} + +const setLang = lang => { + localStorage.setItem('language', lang) + i18n.locale = lang; +} + +export const setLanguage = lang => { + if (i18n.locale !== lang) { + importLanguage(lang).then(setLang) + } +} + +// 组合翻译,例如key为'请输入{0}',keys为login.username,则自动将keys翻译并替换到{0} {1}... +Vue.prototype.$tm = function (key, ...keys) { + let values = []; + for (const k of keys) { + values.push(i18n.t(k)) + } + return i18n.t(key, values); +}; + +// 忽略警告,即:不存在Key直接返回Key +Vue.prototype.$tk = function (key) { + const hasKey = i18n.te(key) + if (hasKey) { + return i18n.t(key) + } + return key +}; + +// 设置当前语言,LOADED_LANGUAGES以外的翻译文件会自动从lang目录获取(如果有的话), 如果不需要动态加载语言文件,直接用setLang +Vue.prototype.$setLang = setLanguage; + +export default i18n; diff --git a/fit2cloud-view/src/i18n/lang/en-US.js b/fit2cloud-view/src/i18n/lang/en-US.js new file mode 100644 index 0000000000..dbb535b258 --- /dev/null +++ b/fit2cloud-view/src/i18n/lang/en-US.js @@ -0,0 +1,13 @@ +import el from "element-ui/lib/locale/lang/en"; +import fu from "fit2cloud-ui/src/locale/lang/en_US"; // 加载fit2cloud的内容 + +const message = { + // TODO +} + +export default { + ...el, + ...fu, + ...message +}; + diff --git a/fit2cloud-view/src/i18n/lang/zh-CN.js b/fit2cloud-view/src/i18n/lang/zh-CN.js new file mode 100644 index 0000000000..037b34a16b --- /dev/null +++ b/fit2cloud-view/src/i18n/lang/zh-CN.js @@ -0,0 +1,54 @@ +import el from "element-ui/lib/locale/lang/zh-CN"; // 加载element的内容 +import fu from "fit2cloud-ui/src/locale/lang/zh-CN"; // 加载fit2cloud的内容 + +const message = { + commons: { + message_box: { + alert: "警告", + confirm: "确认", + prompt: "提示", + }, + button: { + login: "登录", + ok: "确定", + save: "保存", + delete: "删除", + cancel: "取消", + return: "返回", + }, + msg: { + success: "{0}成功", + op_success: "操作成功", + save_success: "保存成功", + delete_success: "删除成功", + }, + validate: { + limit: '长度在 {0} 到 {1} 个字符', + input: "请输入{0}", + select: "请选择{0}", + }, + personal: { + personal_information: "个人信息", + help_documentation: "帮助文档", + exit_system: "退出系统", + } + }, + login: { + username: "用户名", + password: "密码", + title: "登录 FIT2CLOUD", + welcome: "欢迎回来,请输入用户名和密码登录", + expires: '认证信息已过期,请重新登录', + }, + route: { + system_setting: "系统设置", + user_management: "用户管理", + params_setting: "参数设置", + }, +} + +export default { + ...el, + ...fu, + ...message +}; diff --git a/fit2cloud-view/src/i18n/lang/zh-TW.js b/fit2cloud-view/src/i18n/lang/zh-TW.js new file mode 100644 index 0000000000..911e5c64bc --- /dev/null +++ b/fit2cloud-view/src/i18n/lang/zh-TW.js @@ -0,0 +1,10 @@ +import el from "element-ui/lib/locale/lang/zh-TW"; + +const message = { + // TODO +} + +export default { + ...el, + ...message +}; diff --git a/fit2cloud-view/src/icons/index.js b/fit2cloud-view/src/icons/index.js new file mode 100644 index 0000000000..f9a8ec2a9b --- /dev/null +++ b/fit2cloud-view/src/icons/index.js @@ -0,0 +1,12 @@ +import {library} from '@fortawesome/fontawesome-svg-core' +import {fas} from '@fortawesome/free-solid-svg-icons' +import {far} from '@fortawesome/free-regular-svg-icons' +import {fab} from '@fortawesome/free-brands-svg-icons' +import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome' + +export default { + install(Vue) { + library.add(fas, far, fab); + Vue.component('font-awesome-icon', FontAwesomeIcon); + } +} diff --git a/fit2cloud-view/src/main.js b/fit2cloud-view/src/main.js new file mode 100644 index 0000000000..5253c0549e --- /dev/null +++ b/fit2cloud-view/src/main.js @@ -0,0 +1,33 @@ +import Vue from 'vue' +import "@/styles/index.scss" +import Fit2CloudUI from 'fit2cloud-ui'; +import ElementUI from 'element-ui'; +import App from './App.vue' +import i18n from "./i18n"; +import router from './router' +import store from './store' +import icons from './icons' +import plugins from "./plugins"; +import directives from "./directive"; +import "./permission" + +Vue.config.productionTip = false + +Vue.use(ElementUI, { + size: 'small', + i18n: (key, value) => i18n.t(key, value) +}); +Vue.use(Fit2CloudUI, { + i18n: (key, value) => i18n.t(key, value) +}); +Vue.use(icons); +Vue.use(plugins); +Vue.use(directives); + +new Vue({ + el: '#app', + i18n, + router, + store, + render: h => h(App), +}) diff --git a/fit2cloud-view/src/permission.js b/fit2cloud-view/src/permission.js new file mode 100644 index 0000000000..d431c23090 --- /dev/null +++ b/fit2cloud-view/src/permission.js @@ -0,0 +1,57 @@ +import router from './router' +import store from './store' +import NProgress from 'nprogress' +import 'nprogress/nprogress.css' + +NProgress.configure({showSpinner: false}) // NProgress Configuration + +const whiteList = ['/login'] // no redirect whitelist + +const generateRoutes = async (to, from, next) => { + const hasRoles = store.getters.roles && store.getters.roles.length > 0 + if (hasRoles) { + next() + } else { + try { + const {roles} = await store.dispatch('user/getCurrentUser') + const accessRoutes = await store.dispatch('permission/generateRoutes', roles) + router.addRoutes(accessRoutes) + next({...to, replace: true}) + } catch (error) { + await store.dispatch('user/logout') + next(`/login?redirect=${to.path}`) + NProgress.done() + } + } +} + +// 路由前置钩子,根据实际需求修改 +router.beforeEach(async (to, from, next) => { + NProgress.start() + + const isLogin = await store.dispatch('user/isLogin') // 或者user-token/isLogin + + if (isLogin) { + if (to.path === '/login') { + next({path: '/'}) + NProgress.done() + } else { + await generateRoutes(to, from, next) + } + } else { + /* has not login*/ + if (whiteList.indexOf(to.path) !== -1) { + // in the free login whitelist, go directly + next() + } else { + // other pages that do not have permission to access are redirected to the login page. + next(`/login?redirect=${to.path}`) + NProgress.done() + } + } +}) + +router.afterEach(() => { + // finish progress bar + NProgress.done() +}) diff --git a/fit2cloud-view/src/plugins/index.js b/fit2cloud-view/src/plugins/index.js new file mode 100644 index 0000000000..a99e948028 --- /dev/null +++ b/fit2cloud-view/src/plugins/index.js @@ -0,0 +1,9 @@ +import message from "@/plugins/message"; +import request from "@/plugins/request"; + +export default { + install(Vue) { + Vue.use(message); + Vue.use(request); + } +} diff --git a/fit2cloud-view/src/plugins/message.js b/fit2cloud-view/src/plugins/message.js new file mode 100644 index 0000000000..425a40dacd --- /dev/null +++ b/fit2cloud-view/src/plugins/message.js @@ -0,0 +1,71 @@ +import {MessageBox, Message} from 'element-ui'; +import i18n from "@/i18n"; + +export const $alert = (message, callback, options) => { + let title = i18n.t("common.message_box.alert"); + MessageBox.alert(message, title, options).then(() => { + callback(); + }); +} + +export const $confirm = (message, callback, options = {}) => { + let defaultOptions = { + confirmButtonText: i18n.t("common.button.ok"), + cancelButtonText: i18n.t("common.button.cancel"), + type: 'warning', + ...options + } + let title = i18n.t("common.message_box.confirm"); + MessageBox.confirm(message, title, defaultOptions).then(() => { + callback(); + }); +} + +export const $success = (message, duration) => { + Message.success({ + message: message, + type: "success", + showClose: true, + duration: duration || 1500 + }) +} + +export const $info = (message, duration) => { + Message.info({ + message: message, + type: "info", + showClose: true, + duration: duration || 3000 + }) +} + +export const $warning = (message, duration) => { + Message.warning({ + message: message, + type: "warning", + showClose: true, + duration: duration || 5000 + }) +} + +export const $error = (message, duration) => { + Message.error({ + message: message, + type: "error", + showClose: true, + duration: duration || 10000 + }) +} + +export default { + install(Vue) { + // 使用$$前缀,避免与Element UI的冲突 + Vue.prototype.$$confirm = $confirm; + Vue.prototype.$$alert = $alert; + + Vue.prototype.$success = $success; + Vue.prototype.$info = $info; + Vue.prototype.$warning = $warning; + Vue.prototype.$error = $error; + } +} diff --git a/fit2cloud-view/src/plugins/request.js b/fit2cloud-view/src/plugins/request.js new file mode 100644 index 0000000000..5ce85c91c0 --- /dev/null +++ b/fit2cloud-view/src/plugins/request.js @@ -0,0 +1,108 @@ +import axios from 'axios' +import {$alert, $error} from "./message" +import store from '@/store' +import i18n from "@/i18n"; +import {TokenKey, getToken} from '@/utils/token' + +const instance = axios.create({ + baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url + withCredentials: true, + timeout: 60000 // request timeout, default 1 min +}) + +// 每次请求加上Token。如果没用使用Token,删除这个拦截器 +instance.interceptors.request.use( + config => { + if (store.getters.token) { + config.headers[TokenKey] = getToken() + } + return config + }, + error => { + console.log(error) // for debug + return Promise.reject(error) + } +) + +const checkAuth = response => { + // 请根据实际需求修改 + if (response.headers["authentication-status"] === "invalid" || response.status === 401) { + let message = i18n.t('login.expires'); + $alert(message, () => { + store.dispatch('user/logout').then(() => { + location.reload() + }) + }); + } +} + +const checkPermission = response => { + // 请根据实际需求修改 + if (response.status === 403) { + location.href = "/403"; + } +} + +// 请根据实际需求修改 +instance.interceptors.response.use(response => { + checkAuth(response); + return response; +}, error => { + let msg; + if (error.response) { + checkAuth(error.response); + checkPermission(error.response); + msg = error.response.data.message || error.response.data; + } else { + console.log('error: ' + error) // for debug + msg = error.message; + } + $error(msg) + return Promise.reject(error); +}); + +export const request = instance + +/* 简化请求方法,统一处理返回结果,并增加loading处理,这里以{success,data,message}格式的返回值为例,具体项目根据实际需求修改 */ +const promise = (request, loading = {}) => { + return new Promise((resolve, reject) => { + loading.status = true; + request.then(response => { + if (response.data.success) { + resolve(response.data); + } else { + reject(response.data) + } + loading.status = false; + }).catch(error => { + reject(error) + loading.status = false; + }) + }) +} + +export const get = (url, data, loading) => { + return promise(request({url: url, method: "get", params: data}), loading) +}; + +export const post = (url, data, loading) => { + return promise(request({url: url, method: "post", data}), loading) +}; + +export const put = (url, data, loading) => { + return promise(request({url: url, method: "put", data}), loading) +}; + +export const del = (url, loading) => { + return promise(request({url: url, method: "delete"}), loading) +}; + +export default { + install(Vue) { + Vue.prototype.$get = get; + Vue.prototype.$post = post; + Vue.prototype.$put = put; + Vue.prototype.$delete = del; + Vue.prototype.$request = request; + } +} diff --git a/fit2cloud-view/src/router/index.js b/fit2cloud-view/src/router/index.js new file mode 100644 index 0000000000..7178a65383 --- /dev/null +++ b/fit2cloud-view/src/router/index.js @@ -0,0 +1,69 @@ +import Vue from 'vue' +import Router from 'vue-router' + +// 加载modules中的路由 +const modules = require.context('./modules', true, /\.js$/) + +// 修复路由变更后报错的问题 +const routerPush = Router.prototype.push; +Router.prototype.push = function push(location) { + return routerPush.call(this, location).catch(error => error) +} + +Vue.use(Router) + +import Layout from '@/business/app-layout/horizontal-layout' + +export const constantRoutes = [ + { + path: '/redirect', + component: Layout, + hidden: true, + children: [ + { + path: '/redirect/:path(.*)', + component: () => import('@/components/redirect') + } + ] + }, + { + path: '/login', + component: () => import('@/business/login'), + hidden: true + }, + { + path: '/', + component: Layout, + redirect: '/dashboard', + children: [ + { + path: 'dashboard', + component: () => import('@/business/dashboard'), + name: 'Dashboard', + meta: {title: 'Dashboard', icon: 'el-icon-s-marketing', affix: true} + } + ] + } +] + +/** + * 用户登录后根据角色加载的路由 + */ +export const rolesRoutes = [ + ...modules.keys().map(key => modules(key).default), + {path: '*', redirect: '/', hidden: true} +] + +const createRouter = () => new Router({ + scrollBehavior: () => ({y: 0}), + routes: constantRoutes +}) + +const router = createRouter() + +export function resetRouter() { + const newRouter = createRouter() + router.matcher = newRouter.matcher // reset router +} + +export default router diff --git a/fit2cloud-view/src/router/modules/directives.js b/fit2cloud-view/src/router/modules/directives.js new file mode 100644 index 0000000000..f75da30f9e --- /dev/null +++ b/fit2cloud-view/src/router/modules/directives.js @@ -0,0 +1,30 @@ +import Layout from "@/business/app-layout/horizontal-layout"; + +const Directive = { + path: '/directive', + component: Layout, + name: 'Directive', + meta: { + title: "指令示例", + icon: 'el-icon-setting', + }, + children: [ + { + path: 'click-outside', + component: () => import('@/business/directive/ClickOutsideDemo'), + name: "ClickOutside", + meta: { + title: "点击外部指令" + } + }, + { + path: 'permission', + component: () => import('@/business/directive/PermissionDemo'), + name: "Permission", + meta: { + title: "权限指令" + } + } + ] +} +export default Directive diff --git a/fit2cloud-view/src/router/modules/filters.js b/fit2cloud-view/src/router/modules/filters.js new file mode 100644 index 0000000000..405c1a3ff8 --- /dev/null +++ b/fit2cloud-view/src/router/modules/filters.js @@ -0,0 +1,12 @@ +import Layout from "@/business/app-layout/horizontal-layout"; + +const Filters = { + path: '/filters', + component: Layout, + name: 'Filters', + meta: { + title: "过滤器示例", + icon: 'el-icon-setting', + } +} +export default Filters diff --git a/fit2cloud-view/src/router/modules/system-setting.js b/fit2cloud-view/src/router/modules/system-setting.js new file mode 100644 index 0000000000..2a895acfed --- /dev/null +++ b/fit2cloud-view/src/router/modules/system-setting.js @@ -0,0 +1,33 @@ +import Layout from "@/business/app-layout/horizontal-layout"; + +const SystemSetting = { + path: '/system-setting', + component: Layout, + name: 'SystemSetting', + meta: { + title: "route.system_setting", + icon: 'el-icon-setting', + roles: ['admin'] + }, + children: [ + { + path: 'user-management', + component: () => import('@/business/system-setting/UserManagement'), + name: "UserManagement", + meta: { + title: "route.user_management", + roles: ['admin'] + } + }, + { + path: 'params-setting', + component: () => import('@/business/system-setting/ParamsSetting'), + name: "ParamsSetting", + meta: { + title: "route.params_setting", + roles: ['admin'] + } + } + ] +} +export default SystemSetting diff --git a/fit2cloud-view/src/store/getters.js b/fit2cloud-view/src/store/getters.js new file mode 100644 index 0000000000..a589dc74b0 --- /dev/null +++ b/fit2cloud-view/src/store/getters.js @@ -0,0 +1,10 @@ +// 根据实际需要修改 +const getters = { + sidebar: state => state.app.sidebar, + name: state => state.user.name, + language: state => state.user.language, + roles: state => state.user.roles, + permission_routes: state => state.permission.routes, + license: state => state.license, +} +export default getters diff --git a/fit2cloud-view/src/store/index.js b/fit2cloud-view/src/store/index.js new file mode 100644 index 0000000000..99e44bff33 --- /dev/null +++ b/fit2cloud-view/src/store/index.js @@ -0,0 +1,23 @@ +import Vue from 'vue' +import Vuex from 'vuex' +import getters from './getters' + +Vue.use(Vuex) + +// 自动从modules目录下获取模块 +const MODULES_FILES = require.context('./modules', true, /\.js$/) + +// 模块名为js文件名,例如user.js 则模块名为user +const modules = MODULES_FILES.keys().reduce((modules, modulePath) => { + const value = MODULES_FILES(modulePath) + const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') + modules[moduleName] = value.default + return modules +}, {}) + +const store = new Vuex.Store({ + modules, + getters +}) + +export default store diff --git a/fit2cloud-view/src/store/modules/app.js b/fit2cloud-view/src/store/modules/app.js new file mode 100644 index 0000000000..9068d3729b --- /dev/null +++ b/fit2cloud-view/src/store/modules/app.js @@ -0,0 +1,50 @@ +const get = () => { + return localStorage.getItem('sidebarStatus') +} +const set = value => { + localStorage.setItem('sidebarStatus', value) +} +const state = { + sidebar: { + opened: get() ? !!+get() : true + }, + device: 'desktop' +} + +const mutations = { + TOGGLE_SIDEBAR: state => { + state.sidebar.opened = !state.sidebar.opened + if (state.sidebar.opened) { + set(1) + } else { + set(0) + } + }, + OPEN_SIDEBAR: (state) => { + set('sidebarStatus', 1) + state.sidebar.opened = true + }, + CLOSE_SIDEBAR: (state) => { + set('sidebarStatus', 0) + state.sidebar.opened = false + } +} + +const actions = { + toggleSideBar({commit}) { + commit('TOGGLE_SIDEBAR') + }, + openSideBar({commit}) { + commit('OPEN_SIDEBAR') + }, + closeSideBar({commit}) { + commit('CLOSE_SIDEBAR') + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/fit2cloud-view/src/store/modules/license.js b/fit2cloud-view/src/store/modules/license.js new file mode 100644 index 0000000000..dddc77f6b0 --- /dev/null +++ b/fit2cloud-view/src/store/modules/license.js @@ -0,0 +1,64 @@ +import {saveLicense} from "@/api/license" + +const LicenseKey = "X-License"; + +const Status = { + valid: "valid", + invalid: "invalid", + expired: "expired", +} + +const get = () => { + return localStorage.getItem(LicenseKey) +} +const set = value => { + localStorage.setItem(LicenseKey, value) +} +const state = { + status: get(), + license: {}, + message: "" +} + +const mutations = { + SET_STATUS: (state, status) => { + set(LicenseKey, status) + state.status = status; + }, + SET_LICENSE: (state, license) => { + state.license = license; + }, + SET_MESSAGE: (state, message) => { + state.message = message; + } +} + +const actions = { + saveLicense({commit}, content) { + return new Promise((resolve, reject) => { + saveLicense({license: content}).then(response => { + const {status, license, message} = response.data; + commit('SET_STATUS', status) + commit('SET_LICENSE', license) + commit('SET_MESSAGE', message) + resolve(status) + }).catch(error => { + commit('SET_STATUS', Status.invalid) + reject(error) + }) + }) + }, + isValid({state}) { + return state.status === Status.valid + }, + isExpired({state}) { + return state.status === Status.expired + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/fit2cloud-view/src/store/modules/permission.js b/fit2cloud-view/src/store/modules/permission.js new file mode 100644 index 0000000000..57505565eb --- /dev/null +++ b/fit2cloud-view/src/store/modules/permission.js @@ -0,0 +1,61 @@ +import {rolesRoutes, constantRoutes} from '@/router' + +function hasPermission(roles, route) { + if (route.meta && route.meta.roles) { + return roles.some(role => route.meta.roles.includes(role)) + } else { + return true + } +} + +export function filterRolesRoutes(routes, roles) { + const res = [] + + routes.forEach(route => { + const tmp = {...route} + if (hasPermission(roles, tmp)) { + if (tmp.children) { + tmp.children = filterRolesRoutes(tmp.children, roles) + } + res.push(tmp) + } + }) + + return res +} + +const state = { + routes: [], + addRoutes: [] +} + +const mutations = { + SET_ROUTES: (state, routes) => { + state.addRoutes = routes + state.routes = constantRoutes.concat(routes) + } +} + +const actions = { + generateRoutes({commit}, roles) { + return new Promise(resolve => { + let accessedRoutes + if (roles.includes('admin')) { + // admin角色加载所有路由 + accessedRoutes = rolesRoutes || [] + } else { + // 其他角色加载对应角色的路由 + accessedRoutes = filterRolesRoutes(rolesRoutes, roles) + } + commit('SET_ROUTES', accessedRoutes) + resolve(accessedRoutes) + }) + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/fit2cloud-view/src/store/modules/user-token.js b/fit2cloud-view/src/store/modules/user-token.js new file mode 100644 index 0000000000..78735dded6 --- /dev/null +++ b/fit2cloud-view/src/store/modules/user-token.js @@ -0,0 +1,97 @@ +import {login, getCurrentUser, updateInfo, logout} from '@/api/user-token' +import {resetRouter} from '@/router' +import {getToken, setToken, removeToken} from '@/utils/token' +import {getLanguage, setLanguage} from "@/i18n"; + +/* 前后端不分离的登录办法*/ +const state = { + token: getToken(), + name: "", + language: getLanguage(), + roles: [] +} + +const mutations = { + SET_TOKEN: (state, token) => { + state.token = token + }, + SET_NAME: (state, name) => { + state.name = name + }, + SET_LANGUAGE: (state, language) => { + state.language = language + setLanguage(language) + }, + SET_ROLES: (state, roles) => { + state.roles = roles + } +} + +const actions = { + login({commit}, userInfo) { + const {username, password} = userInfo + return new Promise((resolve, reject) => { + login({username: username.trim(), password: password}).then(response => { + let token = response.data + commit('SET_TOKEN', token) + setToken(token) + resolve(response) + }).catch(error => { + reject(error) + }) + }) + }, + + isLogin({commit}) { + return new Promise((resolve, reject) => { + let token = getToken() + if (token) { + commit('SET_TOKEN', token); + resolve(true) + } else { + reject(false) + } + }); + }, + + getCurrentUser({commit}) { + return new Promise((resolve, reject) => { + getCurrentUser().then(response => { + const {name, roles, language} = response.data + commit('SET_NAME', name) + commit('SET_ROLES', roles) + commit('SET_LANGUAGE', language) + resolve(response.data) + }).catch(error => { + reject(error) + }) + }); + }, + + setLanguage({commit, state}, language) { + commit('SET_LANGUAGE', language) + return new Promise((resolve, reject) => { + updateInfo(state.id, {language: language}).then(response => { + resolve(response) + }).catch(error => { + reject(error) + }) + }) + }, + + logout({commit}) { + logout().then(() => { + commit('SET_TOKEN', ""); + commit('SET_ROLES', []) + removeToken() + resetRouter() + }) + }, +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/fit2cloud-view/src/store/modules/user.js b/fit2cloud-view/src/store/modules/user.js new file mode 100644 index 0000000000..6b90773972 --- /dev/null +++ b/fit2cloud-view/src/store/modules/user.js @@ -0,0 +1,99 @@ +/* 前后端不分离的登录方式*/ +import {login, isLogin, getCurrentUser, updateInfo, logout} from '@/api/user' +import {resetRouter} from '@/router' +import {getLanguage, setLanguage} from "@/i18n"; + +const state = { + login: false, + name: "", + language: getLanguage(), + roles: [] +} + +const mutations = { + LOGIN: (state) => { + state.login = true + }, + LOGOUT: (state) => { + state.login = false + }, + SET_NAME: (state, name) => { + state.name = name + }, + SET_LANGUAGE: (state, language) => { + state.language = language + setLanguage(language) + }, + SET_ROLES: (state, roles) => { + state.roles = roles + } +} + +const actions = { + login({commit}, userInfo) { + const {username, password} = userInfo + return new Promise((resolve, reject) => { + login({username: username.trim(), password: password}).then(response => { + commit('LOGIN') + resolve(response) + }).catch(error => { + reject(error) + }) + }) + }, + + isLogin({commit}) { + return new Promise((resolve) => { + if (state.login) { + resolve(true) + return; + } + isLogin().then(() => { + commit('LOGIN') + resolve(true) + }).catch(() => { + resolve(false) + }) + }); + }, + + getCurrentUser({commit}) { + return new Promise((resolve, reject) => { + getCurrentUser().then(response => { + const {name, roles, language} = response.data + commit('SET_NAME', name) + commit('SET_ROLES', roles) + commit('SET_LANGUAGE', language) + resolve(response.data) + }).catch(error => { + reject(error) + }) + }); + }, + + setLanguage({commit, state}, language) { + commit('SET_LANGUAGE', language) + return new Promise((resolve, reject) => { + updateInfo(state.id, {language: language}).then(response => { + resolve(response) + }).catch(error => { + reject(error) + }) + }) + }, + + logout({commit}) { + logout().then(() => { + commit('LOGOUT') + commit('SET_ROLES', []) + resetRouter() + }) + }, +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/fit2cloud-view/src/styles/business/app.scss b/fit2cloud-view/src/styles/business/app.scss new file mode 100644 index 0000000000..8b02be0c82 --- /dev/null +++ b/fit2cloud-view/src/styles/business/app.scss @@ -0,0 +1,54 @@ +@import "~@/assets/font/Roboto/index.css"; + +html { + height: 100%; + box-sizing: border-box; +} + +body { + font-family: Roboto, Helvetica, PingFang SC, Arial, sans-serif; + font-size: 14px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + height: 100%; +} + +#app { + height: 100%; +} + +:focus { + outline: none; +} + +a:active { + outline: none; +} + +a, +a:focus, +a:hover { + cursor: pointer; + color: inherit; + text-decoration: none; +} + +// 滚动条整体部分 +::-webkit-scrollbar { + width: 6px; // 纵向滚动条宽度 + height: 6px; // 横向滚动条高度 +} + +// 滑块 +::-webkit-scrollbar-thumb { + border-radius: 5px; + background-color: #4A4B4D; +} + +// 轨道 +::-webkit-scrollbar-track { + border-radius: 5px; + background-color: transparent; +} + diff --git a/fit2cloud-view/src/styles/business/header-menu.scss b/fit2cloud-view/src/styles/business/header-menu.scss new file mode 100644 index 0000000000..d25e4a8fac --- /dev/null +++ b/fit2cloud-view/src/styles/business/header-menu.scss @@ -0,0 +1,39 @@ +@import "~@/styles/common/variables.scss"; + +.header-menu { + min-width: 150px; + color: #3E3E3D; + + &.el-menu { + background-color: transparent; + + &.el-menu--horizontal { + border: none; + + .el-submenu__title { + border: none; + min-width: 150px; + height: 40px; + line-height: 40px; + } + } + } +} + +.header-menu-popper { + color: #3E3E3D; + + .el-menu--popup { + min-width: 150px; + } + + .el-menu-item { + &.is-active { + color: $--color-primary; + } + + &:hover { + background-color: #D5D5D5; + } + } +} diff --git a/fit2cloud-view/src/styles/common/mixins.scss b/fit2cloud-view/src/styles/common/mixins.scss new file mode 100644 index 0000000000..e365c9bd39 --- /dev/null +++ b/fit2cloud-view/src/styles/common/mixins.scss @@ -0,0 +1,15 @@ +@mixin flex-row($justify: flex-start, $align: stretch) { + display: flex; + @if $justify != flex-start { + justify-content: $justify; + } + @if $align != stretch { + align-items: $align; + } +} + +@mixin click-active-scale($scale:0.95) { + &:active { + transform: scale($scale); + } +} diff --git a/fit2cloud-view/src/styles/common/variables.scss b/fit2cloud-view/src/styles/common/variables.scss new file mode 100644 index 0000000000..3212a7de6d --- /dev/null +++ b/fit2cloud-view/src/styles/common/variables.scss @@ -0,0 +1,48 @@ +/* Element 变量 */ +$--color-primary: #447DF7; +$--color-success: #87CB16; +$--color-warning: #FFA534; +$--color-danger: #FB404B; + +$--box-shadow-light: 0 1px 4px 0 rgb(0 0 0 / 14%); + +$--color-text-primary: #3c4858; + +/* layout */ +$layout-bg-color: #F2F2F2; + +/* sidebar */ +$sidebar-open-width: 260px; +$sidebar-close-width: 80px; +$sidebar-bg-color: #30373d; +$sidebar-bg-gradient: linear-gradient(to bottom right, #30373D, #3E3E3D); + +/* menu */ +$menu-color: #BFCBD9; +$menu-active-color: #FFF; +$menu-active-bg-color: rgb(200 200 200 / 20%); +$menu-bg-color: transparent; +$menu-bg-color-hover: #4A4B4D; +$menu-height: 50px; +$submenu-height: 40px; +$submenu-active-color: #FFF; +$submenu-active-bg-color: $menu-active-bg-color; + +/* logo */ +$logo-height: 40px; +$logo-bg-color: #4E5051; + +/* header */ +$header-height: 60px; +$header-padding: 30px; + +/* main */ +$view-padding: 15px; + +/* fit2cloud-ui的variables加载了element-ui的变量 */ +@import "~fit2cloud-ui/src/styles/common/variables"; + +:export { + theme: $--color-primary; +} + diff --git a/fit2cloud-view/src/styles/index.scss b/fit2cloud-view/src/styles/index.scss new file mode 100644 index 0000000000..f0d62039a0 --- /dev/null +++ b/fit2cloud-view/src/styles/index.scss @@ -0,0 +1,4 @@ +@import '~normalize.css/normalize.css'; +@import "./common/variables"; +@import "~fit2cloud-ui/src/styles"; +@import "./business/app"; diff --git a/fit2cloud-view/src/utils/token.js b/fit2cloud-view/src/utils/token.js new file mode 100644 index 0000000000..21195b9df3 --- /dev/null +++ b/fit2cloud-view/src/utils/token.js @@ -0,0 +1,15 @@ +import Cookies from 'js-cookie' + +export const TokenKey = 'App-Token' // 自行修改 + +export function getToken() { + return Cookies.get(TokenKey) +} + +export function setToken(token) { + return Cookies.set(TokenKey, token) +} + +export function removeToken() { + return Cookies.remove(TokenKey) +} diff --git a/fit2cloud-view/src/utils/validate.js b/fit2cloud-view/src/utils/validate.js new file mode 100644 index 0000000000..7b5dfcb9f6 --- /dev/null +++ b/fit2cloud-view/src/utils/validate.js @@ -0,0 +1,3 @@ +export function isExternal(path) { + return /^(https?:|mailto:|tel:)/.test(path) +} diff --git a/fit2cloud-view/vue.config.js b/fit2cloud-view/vue.config.js new file mode 100644 index 0000000000..e77f332714 --- /dev/null +++ b/fit2cloud-view/vue.config.js @@ -0,0 +1,37 @@ +const path = require('path') + +function resolve(dir) { + return path.join(__dirname, dir) +} + +module.exports = { + productionSourceMap: true, + // 使用mock-server + devServer: { + port: 8080, + open: true, + overlay: { + warnings: false, + errors: true + }, + before: require('./mock/mock-server.js') + }, + // 不使用mock-server,直接连接开发服务器 + // devServer: { + // port: 8080, + // proxy: { + // ['^(?!/login)']: { + // target: 'http://localhost:8081', + // ws: true, + // } + // } + // }, + configureWebpack: { + devtool: 'source-map', + resolve: { + alias: { + '@': resolve('src') + } + } + } +}; diff --git a/fit2cloud-view/代码规范.MD b/fit2cloud-view/代码规范.MD new file mode 100644 index 0000000000..536d7695af --- /dev/null +++ b/fit2cloud-view/代码规范.MD @@ -0,0 +1,23 @@ +####文件命名: +- html 小写字母+横线,例如:index.html,org-list.html +- js 小写字母+横线,例如:i18n.js,en-US.js +- vue 驼峰命名,首字母大写,例如Login.vue,HeaderUser.vue + +####变量命名: +- 常量 大写字母加下划线,例如:const ROLE_ADMIN='admin' +- 变量 驼峰命名,首字母小写,例如let name,let currentProject +- 方法 驼峰命名,首字母小写,例如function open(){},function openDialog() + +####Vue组件: +- 导出名称 驼峰命名,首字母大写,以Ms开头,例如MsUser + +####样式规范: +- 控件的样式写在vue文件的中 +- 公共样式(多个控件使用)写在单独的scss文件中 +- 命名 小写字母+横线,例如.menu,.header-menu,#header-top + +####格式要求: +- 遵循.editorconfig + +####Vue风格指南: +- https://cn.vuejs.org/v2/style-guide/ diff --git a/fit2cloud-view/国际化规范.md b/fit2cloud-view/国际化规范.md new file mode 100644 index 0000000000..385769b4e1 --- /dev/null +++ b/fit2cloud-view/国际化规范.md @@ -0,0 +1,74 @@ +# 国际化文件书写规范 + +### 文件内容 + +每个语言文件由element-ui的国际化内容和自定义国际化内容组成,以zh_CN.js为例: + +```js +import el from "element-ui/lib/locale/lang/zh-CN"; + +const message = { + ... +} + +export default { + ...el, // element-ui的国际化内容 + ...message // 自定义内容 +}; +``` + +### 自定义内容 + +自定义部分按照业务模块划分,通用的写在commons内,例如 + +```js +const message = { + commons: { // 通用 + ... + }, + login: { // 登录 + ... + }, + ... // 其他模块 +} + +``` + +### 层级结构 + +按照业务模块划分后,仍然可以按照子业务或功能再进行划分,但每个业务模块下不要超过3层,例如: + +```js +const message = { + user_manager: { + user_list: { // 用户列表 + name: "姓名", + search: { + ... // 用户列表查询 + }, + ... // 用户列表 + }, + user_edit: { + ... // 编辑用户 + } + }, + ... // 其他模块 +} + +``` + +### Key命名 + +所有Key的命名必须采用英文单词的方式命名,多个单词之间用下划线( _ )连接,尽量让人一看就知道这个key代表的意思, 例如:user_list + +```js +const message = { + user_manager: { + user_list: { + ... + }, + user_edit: {} + }, +} + +``` diff --git a/fit2cloud-view/目录结构.md b/fit2cloud-view/目录结构.md new file mode 100644 index 0000000000..fa0cad2d04 --- /dev/null +++ b/fit2cloud-view/目录结构.md @@ -0,0 +1,33 @@ +# 目录结构 + +```text +├── public // 静态资源 +│ ├── favicon.icon // 图标 +│ └── index.html // 入口html +│ ├── mock // 项目mock 模拟数据 +├── src // 源代码 +│ ├── api // 所有请求 +│ ├── assets // 主题 字体等静态资源 +│ ├── business // 业务组件 +│ ├── components // 全局公用组件 +│ ├── directive // 全局指令 +│ ├── filters // 全局 filter +│ ├── icons // 项目所有 svg icons +│ ├── lang // 国际化 language +│ ├── plugins // Vue插件 +│ ├── router // 路由 +│ ├── store // 全局 store管理 +│ ├── styles // 全局样式 +│ ├── utils // 全局公用方法 +│ ├── App.vue // 入口应用组件 +│ ├── main.js // 入口js +│ └── permission.js // 权限管理 +├── .editorconfig // 代码规范配置 +├── .gitignore // git 忽略项 +├── favicon.ico // favicon图标 +├── index.html // html模板 +├── vue.config.js // 构建配置 +└── package.json // package.json + +``` +