forked from github/dataease
feat: 添加前端代码
This commit is contained in:
parent
0ad50b12ac
commit
4bed85dd9b
17
fit2cloud-view/PENDING.md
Normal file
17
fit2cloud-view/PENDING.md
Normal file
@ -0,0 +1,17 @@
|
||||
# 功能
|
||||
|
||||
- [x] 登录页面
|
||||
- [x] 整体布局
|
||||
- [x] 路由基础框架
|
||||
- [x] 左侧菜单
|
||||
- [x] API基础框架
|
||||
- [x] mock
|
||||
- [x] 国际化及规范
|
||||
- [x] 加载FIT2CLOUD UI
|
||||
- [ ] 权限控制
|
||||
- [ ] 完整Demo路由及页面
|
||||
- [ ] 权限 Demo
|
||||
- [ ] Form Demo
|
||||
- [ ] Table Demo
|
||||
- [ ] 说明文档
|
||||
|
1
fit2cloud-view/README.md
Normal file
1
fit2cloud-view/README.md
Normal file
@ -0,0 +1 @@
|
||||
# FIT2CLOUD 应用模板
|
5
fit2cloud-view/babel.config.js
Normal file
5
fit2cloud-view/babel.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
54
fit2cloud-view/mock/index.js
Normal file
54
fit2cloud-view/mock/index.js
Normal file
@ -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
|
||||
}
|
51
fit2cloud-view/mock/license.js
Normal file
51
fit2cloud-view/mock/license.js
Normal file
@ -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)
|
||||
}
|
||||
},
|
||||
]
|
81
fit2cloud-view/mock/mock-server.js
Normal file
81
fit2cloud-view/mock/mock-server.js
Normal file
@ -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))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
20
fit2cloud-view/mock/result-holder.js
Normal file
20
fit2cloud-view/mock/result-holder.js
Normal file
@ -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
|
||||
}
|
97
fit2cloud-view/mock/user-token.js
Normal file
97
fit2cloud-view/mock/user-token.js
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
]
|
98
fit2cloud-view/mock/user.js
Normal file
98
fit2cloud-view/mock/user.js
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
]
|
48
fit2cloud-view/mock/utils.js
Normal file
48
fit2cloud-view/mock/utils.js
Normal file
@ -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
|
||||
}
|
58
fit2cloud-view/package.json
Normal file
58
fit2cloud-view/package.json
Normal file
@ -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"
|
||||
]
|
||||
}
|
BIN
fit2cloud-view/public/favicon.ico
Normal file
BIN
fit2cloud-view/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
17
fit2cloud-view/public/index.html
Normal file
17
fit2cloud-view/public/index.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
12
fit2cloud-view/src/App.vue
Normal file
12
fit2cloud-view/src/App.vue
Normal file
@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
}
|
||||
</script>
|
7
fit2cloud-view/src/api/license.js
Normal file
7
fit2cloud-view/src/api/license.js
Normal file
@ -0,0 +1,7 @@
|
||||
import {post} from "@/plugins/request"
|
||||
|
||||
export function saveLicense(data) {
|
||||
return post("/samples/license/save", data)
|
||||
}
|
||||
|
||||
|
21
fit2cloud-view/src/api/user-token.js
Normal file
21
fit2cloud-view/src/api/user-token.js
Normal file
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
25
fit2cloud-view/src/api/user.js
Normal file
25
fit2cloud-view/src/api/user.js
Normal file
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
BIN
fit2cloud-view/src/assets/RackShift-assist-white.png
Normal file
BIN
fit2cloud-view/src/assets/RackShift-assist-white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
BIN
fit2cloud-view/src/assets/RackShift-black.png
Normal file
BIN
fit2cloud-view/src/assets/RackShift-black.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 94 KiB |
BIN
fit2cloud-view/src/assets/RackShift-white.png
Normal file
BIN
fit2cloud-view/src/assets/RackShift-white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
BIN
fit2cloud-view/src/assets/font/Roboto/Roboto-Regular.ttf
Executable file
BIN
fit2cloud-view/src/assets/font/Roboto/Roboto-Regular.ttf
Executable file
Binary file not shown.
57
fit2cloud-view/src/assets/font/Roboto/index.css
Normal file
57
fit2cloud-view/src/assets/font/Roboto/index.css
Normal file
@ -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;
|
||||
}
|
||||
|
BIN
fit2cloud-view/src/assets/login-desc.png
Normal file
BIN
fit2cloud-view/src/assets/login-desc.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 355 KiB |
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<el-menu :unique-opened="true"
|
||||
:default-active="language"
|
||||
class="header-menu"
|
||||
text-color="inherit"
|
||||
mode="horizontal">
|
||||
<el-submenu index="1" popper-class="header-menu-popper">
|
||||
<template slot="title">
|
||||
<font-awesome-icon class="language-icon" :icon="['fas', 'globe']"/>
|
||||
<span>{{ languageMap[language] }}</span>
|
||||
</template>
|
||||
<el-menu-item v-for="(value, key) in languageMap" :key="key" :index="key" @click="setLanguage(key)">
|
||||
<span>{{ value }}</span>
|
||||
<i class="el-icon-check" v-if="key === language"/>
|
||||
</el-menu-item>
|
||||
</el-submenu>
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "LanguageSwitch",
|
||||
data() {
|
||||
return {
|
||||
languageMap: {
|
||||
"zh-CN": "中文(简体)",
|
||||
"en-US": "English",
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
language() {
|
||||
return this.$store.getters.language
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setLanguage(lang) {
|
||||
this.$store.dispatch('user/setLanguage', lang).then(() => {
|
||||
// do something
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/styles/business/header-menu.scss";
|
||||
|
||||
.header-menu {
|
||||
.language-icon {
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-menu-popper {
|
||||
.el-icon-check {
|
||||
margin-left: 10px;
|
||||
color: $--color-primary;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<el-menu class="header-menu" text-color="inherit" mode="horizontal">
|
||||
<el-submenu index="none" popper-class="header-menu-popper">
|
||||
<template slot="title">
|
||||
<span>{{ name }}</span>
|
||||
</template>
|
||||
<el-menu-item @click="toPersonal">
|
||||
<span>{{ $t('commons.personal.personal_information') }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item @click="toHelp">
|
||||
<span>{{ $t('commons.personal.help_documentation') }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item @click="logout">
|
||||
<span>{{ $t('commons.personal.exit_system') }}</span>
|
||||
</el-menu-item>
|
||||
</el-submenu>
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters} from "vuex";
|
||||
|
||||
export default {
|
||||
name: "PersonalSetting",
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'name'
|
||||
])
|
||||
},
|
||||
methods: {
|
||||
toPersonal() {
|
||||
|
||||
},
|
||||
toHelp() {
|
||||
window.open("https://github.com/fit2cloud-ui/samples", "_blank");
|
||||
},
|
||||
logout() {
|
||||
this.$store.dispatch("user/logout").then(() => {
|
||||
location.reload()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/styles/business/header-menu.scss";
|
||||
</style>
|
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="horizontal-header">
|
||||
<div class="header-left">
|
||||
<sidebar-toggle-button/>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="navbar-item">
|
||||
<language-switch/>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
<personal-setting/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SidebarToggleButton from "@/components/layout/sidebar/SidebarToggleButton";
|
||||
import LanguageSwitch from "@/business/app-layout/header-components/LanguageSwitch";
|
||||
import PersonalSetting from "@/business/app-layout/header-components/PersonalSetting";
|
||||
|
||||
export default {
|
||||
name: "HorizontalHeader",
|
||||
components: {PersonalSetting, LanguageSwitch, SidebarToggleButton}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "~@/styles/common/mixins";
|
||||
|
||||
.horizontal-header {
|
||||
@include flex-row(flex-start, center);
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
// 先去掉横线看看
|
||||
//&:after {
|
||||
// content: "";
|
||||
// position: absolute;
|
||||
// bottom: 0;
|
||||
// left: 0;
|
||||
// height: 1px;
|
||||
// width: 100%;
|
||||
// background-color: #D5D5D5;
|
||||
//}
|
||||
|
||||
.header-left {
|
||||
@include flex-row(flex-start, center);
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
@include flex-row(flex-end, center);
|
||||
flex: auto;
|
||||
height: 100%;
|
||||
|
||||
.navbar-item {
|
||||
color: #2E2E2E;
|
||||
}
|
||||
|
||||
.navbar-item + .navbar-item {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<layout>
|
||||
<template v-slot:header>
|
||||
<horizontal-header/>
|
||||
</template>
|
||||
</layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Layout from "@/components/layout";
|
||||
import HorizontalHeader from "@/business/app-layout/horizontal-layout/HorizontalHeader";
|
||||
|
||||
export default {
|
||||
name: "HorizontalLayout",
|
||||
components: {HorizontalHeader, Layout},
|
||||
}
|
||||
</script>
|
15
fit2cloud-view/src/business/dashboard/index.vue
Normal file
15
fit2cloud-view/src/business/dashboard/index.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
{{ $t('commons.message_box.prompt') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "dashboard"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
13
fit2cloud-view/src/business/directive/ClickOutsideDemo.vue
Normal file
13
fit2cloud-view/src/business/directive/ClickOutsideDemo.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ClickOutsideDemo"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
49
fit2cloud-view/src/business/directive/PermissionDemo.vue
Normal file
49
fit2cloud-view/src/business/directive/PermissionDemo.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<h2>切换admin、editor、readonly用户看到不同的内容</h2>
|
||||
<br/>
|
||||
|
||||
<div class="permission-block admin" v-permission="['admin']">
|
||||
需要admin角色才能看到, 指令设置:v-permission="['admin']"
|
||||
</div>
|
||||
|
||||
<div class="permission-block editor" v-permission="['editor']">
|
||||
需要editor角色才能看到, 指令设置:v-permission="['editor']"
|
||||
</div>
|
||||
|
||||
<div class="permission-block admin-editor" v-permission="['admin', 'editor']">
|
||||
需要admin或者editor角色才能看到, 指令设置:v-permission="['admin', 'editor']"
|
||||
</div>
|
||||
|
||||
<div class="permission-block">
|
||||
任何人都能看到
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "PermissionDemo"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.permission-block {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
font-size: 18px;
|
||||
|
||||
&.admin {
|
||||
color: #2D61A2;
|
||||
}
|
||||
|
||||
&.admin-editor {
|
||||
color: mix(#2D61A2, #FFBA00)
|
||||
}
|
||||
|
||||
&.editor {
|
||||
color: #FFBA00;
|
||||
}
|
||||
}
|
||||
</style>
|
239
fit2cloud-view/src/business/login/index.vue
Normal file
239
fit2cloud-view/src/business/login/index.vue
Normal file
@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<div class="login-background">
|
||||
<div class="login-container">
|
||||
<el-row type="flex" v-loading="loading">
|
||||
<el-col :span="12">
|
||||
<el-form :model="form" :rules="rules" ref="form" size="default">
|
||||
<div class="login-logo">
|
||||
<img src="../../assets/RackShift-black.png" alt="">
|
||||
</div>
|
||||
<div class="login-title">
|
||||
{{ $t('login.title') }}
|
||||
</div>
|
||||
<div class="login-border"></div>
|
||||
<div class="login-welcome">
|
||||
{{ $t('login.welcome') }}
|
||||
</div>
|
||||
<div class="login-form">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="form.username" :placeholder="$t('login.username')" autofocus/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="form.password" :placeholder="$t('login.password')"
|
||||
show-password maxlength="30" show-word-limit
|
||||
autocomplete="new-password"/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="login-btn">
|
||||
<el-button type="primary" class="submit" @click="submit('form')" size="default">
|
||||
{{ $t('commons.button.login') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="login-msg">
|
||||
{{ msg }}
|
||||
</div>
|
||||
</el-form>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="login-image"></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Login",
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
form: {
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
},
|
||||
rules: {
|
||||
username: [
|
||||
{required: true, message: this.$tm('commons.validate.input', 'login.username'), trigger: 'blur'},
|
||||
],
|
||||
password: [
|
||||
// 先去掉方便测试
|
||||
{required: true, message: this.$tm('commons.validate.input', 'login.password'), trigger: 'blur'},
|
||||
{min: 6, max: 30, message: this.$t('commons.validate.limit', [6, 30]), trigger: 'blur'}
|
||||
]
|
||||
},
|
||||
msg: '',
|
||||
redirect: undefined,
|
||||
otherQuery: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route: {
|
||||
handler: function (route) {
|
||||
const query = route.query
|
||||
if (query) {
|
||||
this.redirect = query.redirect
|
||||
this.otherQuery = this.getOtherQuery(query)
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
document.addEventListener("keydown", this.watchEnter);
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
document.removeEventListener("keydown", this.watchEnter);
|
||||
},
|
||||
methods: {
|
||||
watchEnter(e) {
|
||||
let keyCode = e.keyCode;
|
||||
if (keyCode === 13) {
|
||||
this.submit('form');
|
||||
}
|
||||
},
|
||||
submit(form) {
|
||||
this.$refs[form].validate((valid) => {
|
||||
if (valid) {
|
||||
this.loading = true;
|
||||
this.$store.dispatch('user/login', this.form).then(() => {
|
||||
this.$router.push({path: this.redirect || '/', query: this.otherQuery})
|
||||
this.loading = false
|
||||
}).catch(error => {
|
||||
this.msg = error.message
|
||||
this.loading = false
|
||||
})
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
},
|
||||
getOtherQuery(query) {
|
||||
return Object.keys(query).reduce((acc, cur) => {
|
||||
if (cur !== 'redirect') {
|
||||
acc[cur] = query[cur]
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/common/variables";
|
||||
|
||||
@mixin login-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-background {
|
||||
background-color: $--background-color-base;
|
||||
height: 100%;
|
||||
@include login-center;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
min-width: 900px;
|
||||
width: 1280px;
|
||||
height: 520px;
|
||||
background-color: #FFFFFF;
|
||||
@media only screen and (max-width: 1280px) {
|
||||
width: 900px;
|
||||
height: 380px;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
margin-top: 30px;
|
||||
margin-left: 30px;
|
||||
@media only screen and (max-width: 1280px) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-title {
|
||||
margin-top: 50px;
|
||||
font-size: 32px;
|
||||
letter-spacing: 0;
|
||||
text-align: center;
|
||||
color: #999999;
|
||||
|
||||
@media only screen and (max-width: 1280px) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-border {
|
||||
height: 2px;
|
||||
margin: 20px auto 20px;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
background: $--color-primary;
|
||||
@media only screen and (max-width: 1280px) {
|
||||
margin: 10px auto 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-welcome {
|
||||
margin-top: 50px;
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
letter-spacing: 0;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
@media only screen and (max-width: 1280px) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-top: 30px;
|
||||
padding: 0 40px;
|
||||
|
||||
@media only screen and (max-width: 1280px) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
& ::v-deep .el-input__inner {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
margin-top: 40px;
|
||||
padding: 0 40px;
|
||||
@media only screen and (max-width: 1280px) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.submit {
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.login-msg {
|
||||
margin-top: 10px;
|
||||
padding: 0 40px;
|
||||
color: $--color-danger;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-image {
|
||||
background: url(../../assets/login-desc.png) no-repeat;
|
||||
background-size: cover;
|
||||
width: 100%;
|
||||
height: 520px;
|
||||
@media only screen and (max-width: 1280px) {
|
||||
height: 380px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
13
fit2cloud-view/src/business/system-setting/ParamsSetting.vue
Normal file
13
fit2cloud-view/src/business/system-setting/ParamsSetting.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>参数设置</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ParamsSetting"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<layout-content>
|
||||
<dynamic-table>
|
||||
<fu-search-bar quick-placeholder="按 姓名/邮箱 搜索" :components="components" @exec="search"/>
|
||||
</dynamic-table>
|
||||
</layout-content>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DynamicTable from "@/components/dynamic-table";
|
||||
import LayoutContent from "@/components/layout/LayoutContent";
|
||||
|
||||
export default {
|
||||
name: "UserManagement",
|
||||
components: {LayoutContent, DynamicTable},
|
||||
data() {
|
||||
return {
|
||||
components: [
|
||||
{field: "name", label: "姓名", component: "FuInputComponent", defaultOperator: "eq"},
|
||||
{field: "email", label: "Email", component: "FuInputComponent"},
|
||||
{
|
||||
field: "status",
|
||||
label: "状态",
|
||||
component: "FuSelectComponent",
|
||||
options: [
|
||||
{label: "运行中", value: "Running"},
|
||||
{label: "成功", value: "Success"},
|
||||
{label: "失败", value: "Fail"}
|
||||
],
|
||||
multiple: true
|
||||
},
|
||||
{field: "create_time", label: "创建时间", component: "FuDateTimeComponent"},
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
search(condition) {
|
||||
console.log(condition)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<el-row type="flex" justify="end">
|
||||
<div class="table-pagination">
|
||||
<el-pagination
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="currentPage"
|
||||
:page-sizes="pageSizes"
|
||||
:page-size="pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "TablePagination",
|
||||
props: {
|
||||
page: Object,
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
pageSizes: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return [5, 10, 20, 50, 100]
|
||||
}
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
change: Function
|
||||
},
|
||||
methods: {
|
||||
handleSizeChange: function (size) {
|
||||
this.$emit('update:pageSize', size)
|
||||
this.change();
|
||||
},
|
||||
handleCurrentChange(current) {
|
||||
this.$emit('update:currentPage', current)
|
||||
this.change();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table-pagination {
|
||||
padding-top: 20px;
|
||||
}
|
||||
</style>
|
33
fit2cloud-view/src/components/dynamic-table/index.vue
Normal file
33
fit2cloud-view/src/components/dynamic-table/index.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="dynamic-table">
|
||||
<div class="dynamic-table__header" v-if="$slots.header || header">
|
||||
<slot name="header">{{ header }}</slot>
|
||||
</div>
|
||||
<div class="dynamic-table__body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "DynamicTable",
|
||||
props: {
|
||||
header: {},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "~@/styles/common/mixins.scss";
|
||||
|
||||
.dynamic-table {
|
||||
|
||||
}
|
||||
|
||||
.dynamic-table__header {
|
||||
@include flex-row(flex-start, center);
|
||||
height: 60px;
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
27
fit2cloud-view/src/components/layout/LayoutContent.vue
Normal file
27
fit2cloud-view/src/components/layout/LayoutContent.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="content-container">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "LayoutContent"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/styles/common/variables";
|
||||
|
||||
.content-container {
|
||||
transition: 0.3s;
|
||||
color: $--color-text-primary;
|
||||
background-color: #FFFFFF;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 14%);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
11
fit2cloud-view/src/components/layout/LayoutHeader.vue
Normal file
11
fit2cloud-view/src/components/layout/LayoutHeader.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<header class="header-container">
|
||||
<slot></slot>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "LayoutHeader",
|
||||
}
|
||||
</script>
|
16
fit2cloud-view/src/components/layout/LayoutMain.vue
Normal file
16
fit2cloud-view/src/components/layout/LayoutMain.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<el-container class="main-container" direction="vertical">
|
||||
<slot></slot>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "LayoutMain",
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
25
fit2cloud-view/src/components/layout/LayoutSidebar.vue
Normal file
25
fit2cloud-view/src/components/layout/LayoutSidebar.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<aside :class="['sidebar-container', {'is-collapse': isCollapse}]">
|
||||
<slot>
|
||||
<sidebar/>
|
||||
</slot>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters} from "vuex";
|
||||
import Sidebar from "@/components/layout/sidebar";
|
||||
|
||||
export default {
|
||||
name: "LayoutSidebar",
|
||||
components: {Sidebar},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'sidebar'
|
||||
]),
|
||||
isCollapse() {
|
||||
return !this.sidebar.opened
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
20
fit2cloud-view/src/components/layout/LayoutView.vue
Normal file
20
fit2cloud-view/src/components/layout/LayoutView.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<main class="view-container">
|
||||
<transition name="el-fade-in" mode="out-in">
|
||||
<keep-alive>
|
||||
<router-view :key="key"/>
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "LayoutView",
|
||||
computed: {
|
||||
key() {
|
||||
return this.$route.path
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
66
fit2cloud-view/src/components/layout/index.vue
Normal file
66
fit2cloud-view/src/components/layout/index.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<slot>
|
||||
<layout-sidebar/>
|
||||
<layout-main>
|
||||
<layout-header>
|
||||
<slot name="header"></slot>
|
||||
</layout-header>
|
||||
<layout-view/>
|
||||
</layout-main>
|
||||
</slot>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LayoutSidebar from "@/components/layout/LayoutSidebar";
|
||||
import LayoutMain from "@/components/layout/LayoutMain";
|
||||
import LayoutHeader from "@/components/layout/LayoutHeader";
|
||||
import LayoutView from "@/components/layout/LayoutView";
|
||||
|
||||
export default {
|
||||
name: "Layout",
|
||||
components: {LayoutView, LayoutHeader, LayoutMain, LayoutSidebar},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/styles/common/variables";
|
||||
|
||||
.layout-container {
|
||||
min-width: 1024px;
|
||||
height: 100%;
|
||||
background-color: $layout-bg-color;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
position: relative;
|
||||
transition: width 0.28s;
|
||||
width: $sidebar-open-width;
|
||||
min-width: $sidebar-open-width;
|
||||
background-color: $sidebar-bg-color;
|
||||
background-image: $sidebar-bg-gradient;
|
||||
|
||||
&.is-collapse {
|
||||
width: $sidebar-close-width;
|
||||
min-width: $sidebar-close-width;
|
||||
}
|
||||
}
|
||||
|
||||
.header-container {
|
||||
height: $header-height;
|
||||
padding: 0 $header-padding;
|
||||
}
|
||||
|
||||
.view-container {
|
||||
display: block;
|
||||
flex: auto;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
padding: $view-padding;
|
||||
}
|
||||
</style>
|
26
fit2cloud-view/src/components/layout/sidebar/FixiOSBug.js
Normal file
26
fit2cloud-view/src/components/layout/sidebar/FixiOSBug.js
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
37
fit2cloud-view/src/components/layout/sidebar/Item.vue
Normal file
37
fit2cloud-view/src/components/layout/sidebar/Item.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'MenuItem',
|
||||
functional: true,
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
render(h, context) {
|
||||
const {icon, title} = context.props
|
||||
const vnodes = []
|
||||
|
||||
if (icon) {
|
||||
vnodes.push(<i class={[icon, 'sub-el-icon']}/>)
|
||||
}
|
||||
|
||||
if (title) {
|
||||
vnodes.push(<span slot='title'>{(title)}</span>)
|
||||
}
|
||||
return vnodes
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sub-el-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
43
fit2cloud-view/src/components/layout/sidebar/Link.vue
Normal file
43
fit2cloud-view/src/components/layout/sidebar/Link.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<component :is="type" v-bind="linkProps(to)">
|
||||
<slot/>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {isExternal} from '@/utils/validate'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isExternal() {
|
||||
return isExternal(this.to)
|
||||
},
|
||||
type() {
|
||||
if (this.isExternal) {
|
||||
return 'a'
|
||||
}
|
||||
return 'router-link'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
linkProps(to) {
|
||||
if (this.isExternal) {
|
||||
return {
|
||||
href: to,
|
||||
target: '_blank',
|
||||
rel: 'noopener'
|
||||
}
|
||||
}
|
||||
return {
|
||||
to: to
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
85
fit2cloud-view/src/components/layout/sidebar/Logo.vue
Normal file
85
fit2cloud-view/src/components/layout/sidebar/Logo.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
|
||||
<transition name="sidebar-logo-fade">
|
||||
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
||||
<img v-if="collapseLogo" :src="collapseLogo" class="sidebar-logo" alt="Sidebar Logo">
|
||||
</router-link>
|
||||
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
||||
<img v-if="logo" :src="logo" class="sidebar-logo" alt="Sidebar Logo">
|
||||
</router-link>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SidebarLogo',
|
||||
props: {
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: 'FIT2CLOUD',
|
||||
logo: require('@/assets/RackShift-white.png'),
|
||||
collapseLogo: require('@/assets/RackShift-assist-white.png')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/styles/common/variables";
|
||||
|
||||
.sidebar-logo-container {
|
||||
position: relative;
|
||||
height: $header-height;
|
||||
line-height: $header-height;
|
||||
overflow: hidden;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: #{$sidebar-close-width / 4};
|
||||
height: 1px;
|
||||
width: calc(100% - #{$sidebar-close-width / 2});
|
||||
background-color: hsla(0, 0%, 100%, .5);
|
||||
}
|
||||
|
||||
& .sidebar-logo-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
& .sidebar-logo {
|
||||
margin-left: #{$sidebar-close-width / 4};
|
||||
height: $logo-height;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&.collapse {
|
||||
.sidebar-logo-link {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-logo-fade-enter-active {
|
||||
transition: opacity 0.3s;
|
||||
transition-delay: 0.1s
|
||||
}
|
||||
|
||||
.sidebar-logo-fade-enter,
|
||||
.sidebar-logo-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
97
fit2cloud-view/src/components/layout/sidebar/SidebarItem.vue
Normal file
97
fit2cloud-view/src/components/layout/sidebar/SidebarItem.vue
Normal file
@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div v-if="!item.hidden">
|
||||
<template
|
||||
v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
|
||||
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
|
||||
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-no-dropdown':!isNest}">
|
||||
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="$tk(onlyOneChild.meta.title)"/>
|
||||
</el-menu-item>
|
||||
</app-link>
|
||||
</template>
|
||||
|
||||
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body
|
||||
popper-class="sidebar-popper">
|
||||
<template slot="title">
|
||||
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="$tk(item.meta.title)"/>
|
||||
</template>
|
||||
<sidebar-item
|
||||
v-for="child in item.children"
|
||||
:key="child.path"
|
||||
:is-nest="true"
|
||||
:item="child"
|
||||
:base-path="resolvePath(child.path)"
|
||||
class="nest-menu"
|
||||
/>
|
||||
</el-submenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import path from 'path'
|
||||
import {isExternal} from '@/utils/validate'
|
||||
import Item from './Item'
|
||||
import AppLink from './Link'
|
||||
import FixiOSBug from './FixiOSBug'
|
||||
|
||||
export default {
|
||||
name: 'SidebarItem',
|
||||
components: {Item, AppLink},
|
||||
mixins: [FixiOSBug],
|
||||
props: {
|
||||
// route object
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isNest: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
|
||||
// TODO: refactor with render function
|
||||
this.onlyOneChild = null
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
hasOneShowingChild(children = [], parent) {
|
||||
const showingChildren = children.filter(item => {
|
||||
if (item.hidden) {
|
||||
return false
|
||||
} else {
|
||||
// Temp set(will be used if only has one showing child)
|
||||
this.onlyOneChild = item
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
// When there is only one child router, the child router is displayed by default
|
||||
if (showingChildren.length === 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Show parent if there are no child router to display
|
||||
if (showingChildren.length === 0) {
|
||||
this.onlyOneChild = {...parent, path: '', noShowingChildren: true}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
resolvePath(routePath) {
|
||||
if (isExternal(routePath)) {
|
||||
return routePath
|
||||
}
|
||||
if (isExternal(this.basePath)) {
|
||||
return this.basePath
|
||||
}
|
||||
return path.resolve(this.basePath, routePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<el-button circle :class="['sidebar-toggle-button', icon]" @click="toggle"></el-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters} from "vuex";
|
||||
|
||||
export default {
|
||||
name: "SidebarToggleButton",
|
||||
methods: {
|
||||
toggle() {
|
||||
this.$store.dispatch('app/toggleSideBar');
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'sidebar'
|
||||
]),
|
||||
icon() {
|
||||
return this.sidebar.opened ? "el-icon-s-fold" : "el-icon-s-unfold"
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar-toggle-button.el-button {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
</style>
|
266
fit2cloud-view/src/components/layout/sidebar/index.vue
Normal file
266
fit2cloud-view/src/components/layout/sidebar/index.vue
Normal file
@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<div class="sidebar">
|
||||
<logo :collapse="isCollapse"/>
|
||||
<el-scrollbar wrap-class="scrollbar-wrapper">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
:unique-opened="false"
|
||||
mode="vertical">
|
||||
<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path"/>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters} from 'vuex'
|
||||
import SidebarItem from './SidebarItem'
|
||||
import Logo from "@/components/layout/sidebar/Logo";
|
||||
|
||||
export default {
|
||||
name: "Sidebar",
|
||||
components: {Logo, SidebarItem},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'permission_routes',
|
||||
'sidebar'
|
||||
]),
|
||||
activeMenu() {
|
||||
const route = this.$route
|
||||
const {meta, path} = route
|
||||
if (meta.activeMenu) {
|
||||
return meta.activeMenu
|
||||
}
|
||||
return path
|
||||
},
|
||||
isCollapse() {
|
||||
return !this.sidebar.opened
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/styles/common/variables";
|
||||
|
||||
@mixin sidebar-base-item {
|
||||
padding-left: 10px !important;
|
||||
border-radius: 2px;
|
||||
color: $menu-color;
|
||||
}
|
||||
|
||||
@mixin menu-item {
|
||||
@include sidebar-base-item;
|
||||
margin: 2px 10px;
|
||||
line-height: $menu-height;
|
||||
height: $menu-height;
|
||||
}
|
||||
|
||||
@mixin submenu-item {
|
||||
@include sidebar-base-item;
|
||||
margin: 2px 10px;
|
||||
line-height: $submenu-height;
|
||||
height: $submenu-height;
|
||||
}
|
||||
|
||||
@mixin popper-submenu-item {
|
||||
@include sidebar-base-item;
|
||||
margin: 2px 0;
|
||||
line-height: $submenu-height;
|
||||
height: $submenu-height;
|
||||
}
|
||||
|
||||
@mixin menu-item-active {
|
||||
color: $menu-active-color;
|
||||
background-color: $menu-active-bg-color;
|
||||
}
|
||||
|
||||
@mixin submenu-item-active {
|
||||
color: $submenu-active-color;
|
||||
background-color: $submenu-active-bg-color;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
height: 100%;
|
||||
|
||||
.horizontal-collapse-transition {
|
||||
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
|
||||
}
|
||||
|
||||
.el-scrollbar {
|
||||
box-sizing: border-box;
|
||||
padding: 10px 0;
|
||||
height: calc(100% - #{$header-height});
|
||||
|
||||
.el-scrollbar__bar {
|
||||
&.is-vertical {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.is-horizontal {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollbar-wrapper {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: $menu-bg-color;
|
||||
|
||||
.submenu-title-no-dropdown {
|
||||
@include menu-item;
|
||||
|
||||
&:hover {
|
||||
background-color: $menu-bg-color-hover;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
@include menu-item-active;
|
||||
}
|
||||
}
|
||||
|
||||
.el-submenu {
|
||||
.el-submenu__title {
|
||||
@include menu-item;
|
||||
|
||||
&:hover {
|
||||
background-color: $menu-bg-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
.el-submenu__title, {
|
||||
@include menu-item-active;
|
||||
|
||||
.sub-el-icon, span {
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item {
|
||||
@include submenu-item;
|
||||
|
||||
&:hover {
|
||||
background-color: $menu-bg-color-hover;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
@include submenu-item-active
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nest-menu, .el-submenu__title, .submenu-title-no-dropdown {
|
||||
span {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.sub-el-icon {
|
||||
margin-right: 10px;
|
||||
|
||||
+ span {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.el-menu--collapse {
|
||||
.el-tooltip {
|
||||
padding: 0 !important;
|
||||
text-align: center;
|
||||
line-height: $menu-height;
|
||||
}
|
||||
|
||||
.el-submenu__title {
|
||||
padding-left: 20px !important;
|
||||
}
|
||||
|
||||
.submenu-title-no-dropdown, .el-submenu__title {
|
||||
max-width: 60px;
|
||||
text-align: center;
|
||||
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sub-el-icon {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-submenu__icon-arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-popper {
|
||||
& > .el-menu {
|
||||
display: block;
|
||||
background-color: $sidebar-bg-color;
|
||||
|
||||
.sub-el-icon {
|
||||
margin-right: 12px;
|
||||
margin-left: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.nest-menu .el-submenu > .el-submenu__title, .el-menu-item {
|
||||
&.is-active {
|
||||
@include submenu-item-active
|
||||
}
|
||||
|
||||
@include popper-submenu-item;
|
||||
|
||||
span {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.sub-el-icon {
|
||||
margin-right: 10px;
|
||||
|
||||
+ span {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $menu-bg-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
> .el-menu--popup {
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar-track-piece {
|
||||
background: #d3dce6;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #99a9bf;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
12
fit2cloud-view/src/components/redirect/index.vue
Normal file
12
fit2cloud-view/src/components/redirect/index.vue
Normal file
@ -0,0 +1,12 @@
|
||||
<script>
|
||||
export default {
|
||||
created() {
|
||||
const { params, query } = this.$route
|
||||
const { path } = params
|
||||
this.$router.replace({ path: '/' + path, query })
|
||||
},
|
||||
render: function(h) {
|
||||
return h() // avoid warning message
|
||||
}
|
||||
}
|
||||
</script>
|
8
fit2cloud-view/src/directive/click-outside/index.js
Normal file
8
fit2cloud-view/src/directive/click-outside/index.js
Normal file
@ -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
|
11
fit2cloud-view/src/directive/index.js
Normal file
11
fit2cloud-view/src/directive/index.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
29
fit2cloud-view/src/directive/permission/index.js
Normal file
29
fit2cloud-view/src/directive/permission/index.js
Normal file
@ -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)
|
||||
}
|
||||
}
|
75
fit2cloud-view/src/i18n/index.js
Normal file
75
fit2cloud-view/src/i18n/index.js
Normal file
@ -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;
|
13
fit2cloud-view/src/i18n/lang/en-US.js
Normal file
13
fit2cloud-view/src/i18n/lang/en-US.js
Normal file
@ -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
|
||||
};
|
||||
|
54
fit2cloud-view/src/i18n/lang/zh-CN.js
Normal file
54
fit2cloud-view/src/i18n/lang/zh-CN.js
Normal file
@ -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
|
||||
};
|
10
fit2cloud-view/src/i18n/lang/zh-TW.js
Normal file
10
fit2cloud-view/src/i18n/lang/zh-TW.js
Normal file
@ -0,0 +1,10 @@
|
||||
import el from "element-ui/lib/locale/lang/zh-TW";
|
||||
|
||||
const message = {
|
||||
// TODO
|
||||
}
|
||||
|
||||
export default {
|
||||
...el,
|
||||
...message
|
||||
};
|
12
fit2cloud-view/src/icons/index.js
Normal file
12
fit2cloud-view/src/icons/index.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
33
fit2cloud-view/src/main.js
Normal file
33
fit2cloud-view/src/main.js
Normal file
@ -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),
|
||||
})
|
57
fit2cloud-view/src/permission.js
Normal file
57
fit2cloud-view/src/permission.js
Normal file
@ -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()
|
||||
})
|
9
fit2cloud-view/src/plugins/index.js
Normal file
9
fit2cloud-view/src/plugins/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
import message from "@/plugins/message";
|
||||
import request from "@/plugins/request";
|
||||
|
||||
export default {
|
||||
install(Vue) {
|
||||
Vue.use(message);
|
||||
Vue.use(request);
|
||||
}
|
||||
}
|
71
fit2cloud-view/src/plugins/message.js
Normal file
71
fit2cloud-view/src/plugins/message.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
108
fit2cloud-view/src/plugins/request.js
Normal file
108
fit2cloud-view/src/plugins/request.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
69
fit2cloud-view/src/router/index.js
Normal file
69
fit2cloud-view/src/router/index.js
Normal file
@ -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
|
30
fit2cloud-view/src/router/modules/directives.js
Normal file
30
fit2cloud-view/src/router/modules/directives.js
Normal file
@ -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
|
12
fit2cloud-view/src/router/modules/filters.js
Normal file
12
fit2cloud-view/src/router/modules/filters.js
Normal file
@ -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
|
33
fit2cloud-view/src/router/modules/system-setting.js
Normal file
33
fit2cloud-view/src/router/modules/system-setting.js
Normal file
@ -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
|
10
fit2cloud-view/src/store/getters.js
Normal file
10
fit2cloud-view/src/store/getters.js
Normal file
@ -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
|
23
fit2cloud-view/src/store/index.js
Normal file
23
fit2cloud-view/src/store/index.js
Normal file
@ -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
|
50
fit2cloud-view/src/store/modules/app.js
Normal file
50
fit2cloud-view/src/store/modules/app.js
Normal file
@ -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
|
||||
}
|
64
fit2cloud-view/src/store/modules/license.js
Normal file
64
fit2cloud-view/src/store/modules/license.js
Normal file
@ -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
|
||||
}
|
61
fit2cloud-view/src/store/modules/permission.js
Normal file
61
fit2cloud-view/src/store/modules/permission.js
Normal file
@ -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
|
||||
}
|
97
fit2cloud-view/src/store/modules/user-token.js
Normal file
97
fit2cloud-view/src/store/modules/user-token.js
Normal file
@ -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
|
||||
}
|
99
fit2cloud-view/src/store/modules/user.js
Normal file
99
fit2cloud-view/src/store/modules/user.js
Normal file
@ -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
|
||||
}
|
54
fit2cloud-view/src/styles/business/app.scss
Normal file
54
fit2cloud-view/src/styles/business/app.scss
Normal file
@ -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;
|
||||
}
|
||||
|
39
fit2cloud-view/src/styles/business/header-menu.scss
Normal file
39
fit2cloud-view/src/styles/business/header-menu.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
15
fit2cloud-view/src/styles/common/mixins.scss
Normal file
15
fit2cloud-view/src/styles/common/mixins.scss
Normal file
@ -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);
|
||||
}
|
||||
}
|
48
fit2cloud-view/src/styles/common/variables.scss
Normal file
48
fit2cloud-view/src/styles/common/variables.scss
Normal file
@ -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;
|
||||
}
|
||||
|
4
fit2cloud-view/src/styles/index.scss
Normal file
4
fit2cloud-view/src/styles/index.scss
Normal file
@ -0,0 +1,4 @@
|
||||
@import '~normalize.css/normalize.css';
|
||||
@import "./common/variables";
|
||||
@import "~fit2cloud-ui/src/styles";
|
||||
@import "./business/app";
|
15
fit2cloud-view/src/utils/token.js
Normal file
15
fit2cloud-view/src/utils/token.js
Normal file
@ -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)
|
||||
}
|
3
fit2cloud-view/src/utils/validate.js
Normal file
3
fit2cloud-view/src/utils/validate.js
Normal file
@ -0,0 +1,3 @@
|
||||
export function isExternal(path) {
|
||||
return /^(https?:|mailto:|tel:)/.test(path)
|
||||
}
|
37
fit2cloud-view/vue.config.js
Normal file
37
fit2cloud-view/vue.config.js
Normal file
@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
23
fit2cloud-view/代码规范.MD
Normal file
23
fit2cloud-view/代码规范.MD
Normal file
@ -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文件的<style scoped></style>中
|
||||
- 公共样式(多个控件使用)写在单独的scss文件中
|
||||
- 命名 小写字母+横线,例如.menu,.header-menu,#header-top
|
||||
|
||||
####格式要求:
|
||||
- 遵循.editorconfig
|
||||
|
||||
####Vue风格指南:
|
||||
- https://cn.vuejs.org/v2/style-guide/
|
74
fit2cloud-view/国际化规范.md
Normal file
74
fit2cloud-view/国际化规范.md
Normal file
@ -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: {}
|
||||
},
|
||||
}
|
||||
|
||||
```
|
33
fit2cloud-view/目录结构.md
Normal file
33
fit2cloud-view/目录结构.md
Normal file
@ -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
|
||||
|
||||
```
|
||||
|
Loading…
Reference in New Issue
Block a user