first commit

This commit is contained in:
Shu Guang 2025-05-24 10:50:19 +08:00
commit 7f3cdf3ad1
129 changed files with 26462 additions and 0 deletions

14
.editorconfig Normal file
View File

@ -0,0 +1,14 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

5
.env.development Normal file
View File

@ -0,0 +1,5 @@
# just a flag
ENV = 'development'
# base api
VUE_APP_BASE_API = '/dev-api'

7
.env.production Normal file
View File

@ -0,0 +1,7 @@
# just a flag
ENV = 'production'
# base api
# 配置api
VUE_APP_BASE_API = 'http://127.0.0.1:8898/api'

8
.env.staging Normal file
View File

@ -0,0 +1,8 @@
NODE_ENV = production
# just a flag
ENV = 'staging'
# base api
VUE_APP_BASE_API = '/stage-api'

4
.eslintignore Normal file
View File

@ -0,0 +1,4 @@
build/*.js
src/assets
public
dist

198
.eslintrc.js Normal file
View File

@ -0,0 +1,198 @@
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
},
env: {
browser: true,
node: true,
es6: true,
},
extends: ['plugin:vue/recommended', 'eslint:recommended'],
// add your custom rules here
//it is base on https://github.com/vuejs/eslint-config-vue
rules: {
"vue/max-attributes-per-line": [2, {
"singleline": 10,
"multiline": {
"max": 1,
"allowFirstLine": false
}
}],
"vue/singleline-html-element-content-newline": "off",
"vue/multiline-html-element-content-newline":"off",
"vue/name-property-casing": ["error", "PascalCase"],
"vue/no-v-html": "off",
'accessor-pairs': 2,
'arrow-spacing': [2, {
'before': true,
'after': true
}],
'block-spacing': [2, 'always'],
'brace-style': [2, '1tbs', {
'allowSingleLine': true
}],
'camelcase': [0, {
'properties': 'always'
}],
'comma-dangle': [2, 'never'],
'comma-spacing': [2, {
'before': false,
'after': true
}],
'comma-style': [2, 'last'],
'constructor-super': 2,
'curly': [2, 'multi-line'],
'dot-location': [2, 'property'],
'eol-last': 2,
'eqeqeq': ["error", "always", {"null": "ignore"}],
'generator-star-spacing': [2, {
'before': true,
'after': true
}],
'handle-callback-err': [2, '^(err|error)$'],
'indent': [2, 2, {
'SwitchCase': 1
}],
'jsx-quotes': [2, 'prefer-single'],
'key-spacing': [2, {
'beforeColon': false,
'afterColon': true
}],
'keyword-spacing': [2, {
'before': true,
'after': true
}],
'new-cap': [2, {
'newIsCap': true,
'capIsNew': false
}],
'new-parens': 2,
'no-array-constructor': 2,
'no-caller': 2,
'no-console': 'off',
'no-class-assign': 2,
'no-cond-assign': 2,
'no-const-assign': 2,
'no-control-regex': 0,
'no-delete-var': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-empty-pattern': 2,
'no-eval': 2,
'no-ex-assign': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-boolean-cast': 2,
'no-extra-parens': [2, 'functions'],
'no-fallthrough': 2,
'no-floating-decimal': 2,
'no-func-assign': 2,
'no-implied-eval': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': [2, {
'allowLoop': false,
'allowSwitch': false
}],
'no-lone-blocks': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-multiple-empty-lines': [2, {
'max': 1
}],
'no-native-reassign': 2,
'no-negated-in-lhs': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-new-symbol': 2,
'no-new-wrappers': 2,
'no-obj-calls': 2,
'no-octal': 2,
'no-octal-escape': 2,
'no-path-concat': 2,
'no-proto': 2,
'no-redeclare': 2,
'no-regex-spaces': 2,
'no-return-assign': [2, 'except-parens'],
'no-self-assign': 2,
'no-self-compare': 2,
'no-sequences': 2,
'no-shadow-restricted-names': 2,
'no-spaced-func': 2,
'no-sparse-arrays': 2,
'no-this-before-super': 2,
'no-throw-literal': 2,
'no-trailing-spaces': 2,
'no-undef': 2,
'no-undef-init': 2,
'no-unexpected-multiline': 2,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': [2, {
'defaultAssignment': false
}],
'no-unreachable': 2,
'no-unsafe-finally': 2,
'no-unused-vars': [2, {
'vars': 'all',
'args': 'none'
}],
'no-useless-call': 2,
'no-useless-computed-key': 2,
'no-useless-constructor': 2,
'no-useless-escape': 0,
'no-whitespace-before-property': 2,
'no-with': 2,
'one-var': [2, {
'initialized': 'never'
}],
'operator-linebreak': [2, 'after', {
'overrides': {
'?': 'before',
':': 'before'
}
}],
'padded-blocks': [2, 'never'],
'quotes': [2, 'single', {
'avoidEscape': true,
'allowTemplateLiterals': true
}],
'semi': [2, 'never'],
'semi-spacing': [2, {
'before': false,
'after': true
}],
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [2, {
'words': true,
'nonwords': false
}],
'spaced-comment': [2, 'always', {
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}],
'template-curly-spacing': [2, 'never'],
'use-isnan': 2,
'valid-typeof': 2,
'wrap-iife': [2, 'any'],
'yield-star-spacing': [2, 'both'],
'yoda': [2, 'never'],
'prefer-const': 2,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'object-curly-spacing': [2, 'always', {
objectsInObjects: false
}],
'array-bracket-spacing': [2, 'never']
}
}

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
tests/**/coverage/
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln

5
.travis.yml Normal file
View File

@ -0,0 +1,5 @@
language: node_js
node_js: 10
script: npm run test
notifications:
email: false

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017-present PanJiaChen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

14
babel.config.js Normal file
View File

@ -0,0 +1,14 @@
module.exports = {
presets: [
// https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
'@vue/cli-plugin-babel/preset'
],
'env': {
'development': {
// babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
// This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
// https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html
'plugins': ['dynamic-import-node']
}
}
}

35
build/index.js Normal file
View File

@ -0,0 +1,35 @@
const { run } = require('runjs')
const chalk = require('chalk')
const config = require('../vue.config.js')
const rawArgv = process.argv.slice(2)
const args = rawArgv.join(' ')
if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
const report = rawArgv.includes('--report')
run(`vue-cli-service build ${args}`)
const port = 9527
const publicPath = config.publicPath
var connect = require('connect')
var serveStatic = require('serve-static')
const app = connect()
app.use(
publicPath,
serveStatic('./dist', {
index: ['index.html', '/']
})
)
app.listen(port, function () {
console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`))
if (report) {
console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`))
}
})
} else {
run(`vue-cli-service build ${args}`)
}

BIN
dist.zip Normal file

Binary file not shown.

24
jest.config.js Normal file
View File

@ -0,0 +1,24 @@
module.exports = {
moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
transform: {
'^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
snapshotSerializers: ['jest-serializer-vue'],
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
],
collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
coverageDirectory: '<rootDir>/tests/unit/coverage',
// 'collectCoverage': true,
'coverageReporters': [
'lcov',
'text-summary'
],
testURL: 'http://localhost/'
}

9
jsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

57
mock/index.js Normal file
View File

@ -0,0 +1,57 @@
const Mock = require('mockjs')
const { param2Obj } = require('./utils')
const user = require('./user')
const table = require('./table')
const mocks = [
...user,
...table
]
// 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 = null
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
}

81
mock/mock-server.js Normal file
View 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))
}
}
})
}

29
mock/table.js Normal file
View File

@ -0,0 +1,29 @@
const Mock = require('mockjs')
const data = Mock.mock({
'items|30': [{
id: '@id',
title: '@sentence(10, 20)',
'status|1': ['published', 'draft', 'deleted'],
author: 'name',
display_time: '@datetime',
pageviews: '@integer(300, 5000)'
}]
})
module.exports = [
{
url: '/vue-admin-template/table/list',
type: 'get',
response: config => {
const items = data.items
return {
code: 20000,
data: {
total: items.length,
items: items
}
}
}
}
]

84
mock/user.js Normal file
View File

@ -0,0 +1,84 @@
const tokens = {
admin: {
token: 'admin-token'
},
editor: {
token: 'editor-token'
}
}
const users = {
'admin-token': {
roles: ['admin'],
introduction: 'I am a super administrator',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Super Admin'
},
'editor-token': {
roles: ['editor'],
introduction: 'I am an editor',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Normal Editor'
}
}
module.exports = [
// user login
{
url: '/vue-admin-template/user/login',
type: 'post',
response: config => {
const { username } = config.body
const token = tokens[username]
// mock error
if (!token) {
return {
code: 60204,
message: 'Account and password are incorrect.'
}
}
return {
code: 20000,
data: token
}
}
},
// get user info
{
url: '/vue-admin-template/user/info\.*',
type: 'get',
response: config => {
const { token } = config.query
const info = users[token]
// mock error
if (!info) {
return {
code: 50008,
message: 'Login failed, unable to get user details.'
}
}
return {
code: 20000,
data: info
}
}
},
// user logout
{
url: '/vue-admin-template/user/logout',
type: 'post',
response: _ => {
return {
code: 20000,
data: 'success'
}
}
}
]

25
mock/utils.js Normal file
View File

@ -0,0 +1,25 @@
/**
* @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
}
module.exports = {
param2Obj
}

70
package.json Normal file
View File

@ -0,0 +1,70 @@
{
"name": "vue-admin-template",
"version": "4.4.0",
"description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
"author": "Pan <panfree23@gmail.com>",
"scripts": {
"dev": "vue-cli-service serve",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview",
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml",
"lint": "eslint --ext .js,.vue src",
"test:unit": "jest --clearCache && vue-cli-service test:unit",
"test:ci": "npm run lint && npm run test:unit"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"axios": "0.18.1",
"core-js": "3.6.5",
"echarts": "^5.4.1",
"element-china-area-data": "^6.1.0",
"element-ui": "2.13.2",
"js-cookie": "2.2.0",
"js-sha1": "^0.6.0",
"moment": "^2.29.4",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"qs": "^6.11.0",
"swiper": "^6.8.4",
"vue": "2.6.10",
"vue-router": "3.0.6",
"vuex": "3.6.2",
"wangeditor": "^4.7.15"
},
"devDependencies": {
"@vue/cli-plugin-babel": "4.4.4",
"@vue/cli-plugin-eslint": "4.4.4",
"@vue/cli-plugin-unit-jest": "4.4.4",
"@vue/cli-service": "4.4.4",
"@vue/test-utils": "1.0.0-beta.29",
"autoprefixer": "9.5.1",
"babel-eslint": "10.1.0",
"babel-jest": "23.6.0",
"babel-plugin-dynamic-import-node": "2.3.3",
"chalk": "2.4.2",
"connect": "3.6.6",
"eslint": "6.7.2",
"eslint-plugin-vue": "6.2.2",
"html-webpack-plugin": "3.2.0",
"mockjs": "1.0.1-beta3",
"runjs": "4.3.2",
"sass": "1.26.8",
"sass-loader": "^7.3.1",
"script-ext-html-webpack-plugin": "2.1.3",
"serve-static": "1.13.2",
"svg-sprite-loader": "4.1.3",
"svgo": "1.2.2",
"vue-template-compiler": "2.6.10"
},
"browserslist": [
"> 1%",
"last 2 versions"
],
"engines": {
"node": ">=8.9",
"npm": ">= 3.0.0"
},
"license": "MIT"
}

8
postcss.config.js Normal file
View File

@ -0,0 +1,8 @@
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
'plugins': {
// to edit target browsers: use "browserslist" field in package.json
'autoprefixer': {}
}
}

15020
public/address.json Normal file

File diff suppressed because it is too large Load Diff

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

20
public/index.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>基于Spring Boot的停车场管理系统-门户端</title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= webpackConfig.name %> 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>

11
src/App.vue Normal file
View File

@ -0,0 +1,11 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>

56
src/api/brand.js Normal file
View File

@ -0,0 +1,56 @@
// 车辆品牌接口
import request from '@/utils/request'
import qs from 'qs'
// 获取车辆品牌列表
export const getBrandList = (data) => {
return request({
url: '/AllBrandpage',
method: 'POST',
data: qs.stringify(data)
})
}
// 新增车辆品牌
export const addBrand = (data) => {
return request({
url: '/brand/add',
method: 'POST',
data: qs.stringify(data)
})
}
// 删除车辆品牌
export const deleteBrand = (id) => {
return request({
url: `/brand/delete?id=${id}`,
method: 'GET'
})
}
// 修改车辆品牌状态
export const changeStatus = (id, status) => {
return request({
url: `/brand/changeStatus?id=${id}&status=${status}`,
method: 'GET'
})
}
// 获取车辆品牌详情
export const getBrandDetail = (id) => {
return request({
url: `/brand/detail?id=${id}`,
method: 'GET'
})
}
// 修改车辆品牌
export const editBrand = (data) => {
return request({
url: '/brand/edit',
method: 'POST',
data: qs.stringify(data)
})
}

34
src/api/car.js Normal file
View File

@ -0,0 +1,34 @@
// 车辆管理接口
import request from '@/utils/request'
import qs from 'qs'
//租借车位
export const carRent = (data) => {
return request({
url: '/carRent',
method: 'POST',
data: qs.stringify(data)
})
}
// 还车
export const returnCar = (data) => {
return request({
url: '/returnCar',
method: 'POST',
data: qs.stringify(data)
})
}
// 获取停车场的车辆列表
export const getCars = (id) => {
return request({
url: `/getPid?parking_id=${id}`,
method: 'GET'
})
}
// 通过停车场ID查询车位列表
export const getCarList = (id) => {
return request({
url: `/getCarsInParking?parking_id=${id}`,
})
}

22
src/api/comment.js Normal file
View File

@ -0,0 +1,22 @@
// 评论接口
// 修改格式
import qs from 'qs'
import request from '@/utils/request'
// 发布评论
export const push = (data) => {
return request({
url: '/comment',
method: 'POST',
data: qs.stringify(data)
})
}
// 获取个人信息接口
export function getInfo(data) {
return request({
url: '/FindUserById',
method: 'post',
data: qs.stringify(data)
})
}

34
src/api/common.js Normal file
View File

@ -0,0 +1,34 @@
// 公共接口
// 修改格式
import qs from 'qs'
import request from '@/utils/request'
// 导入axios
import axios from 'axios';
// 获取城市列表
// 模拟异步获取城市数据的函数
//
export const getCity = (data) => {
return request({
url: '/cityPicker',
method: 'POST',
data: qs.stringify(data)
})
}
/**
* 封装的table组件请求数据
*/
export const tableData = (params) => {
return request({
url: params.url,
method: params.method,
data: qs.stringify(params.data)
})
}

84
src/api/forum.js Normal file
View File

@ -0,0 +1,84 @@
// 公共接口
// 修改格式
import qs from 'qs'
import request from '@/utils/request'
// 导入axios
import axios from 'axios';
export function getCategories() {
return request({
url: '/forum/categories',
method: 'get'
})
}
export function getPosts(params) {
return request({
url: '/forum/posts',
method: 'get',
params
})
}
export function getPostDetail(id) {
return request({
url: `/forum/posts/${id}`,
method: 'get'
})
}
export function createPost(data) {
// 使用 URLSearchParams 构建查询参数
const params = new URLSearchParams();
params.append('title', data.title);
params.append('content', data.content);
params.append('categoryId', data.categoryId);
params.append('userId', data.userId);
return request({
url: '/forum/post', // 后端接口路径
method: 'get', // 使用 GET 请求
params: params // 通过 params 传递查询参数
});
}
// 获取帖子的所有回复
export function getReplies(postId) {
return request({
url: `/forum/posts/${postId}/replies`,
method: 'GET'
});
}
// 创建新的回复
export function createReply(postId, replyData,userId) {
// 创建FormData对象
const formData = new FormData();
// 添加回复内容
formData.append('content', replyData);
formData.append('userId', userId);
formData.append('postId', postId);
return request({
url: `/forum/posts/${postId}/repliesed`,
method: 'POST',
data: formData,
});
}
// 修改回复内容
export function updateReply(id, replyData) {
return request({
url: `/forum/replies/${id}`,
method: 'put',
data: replyData // 更新的回复数据
});
}
// 删除回复
export function deleteReply(id) {
return request({
url: `/forum/replies/${id}`,
method: 'delete'
});
}

20
src/api/index.js Normal file
View File

@ -0,0 +1,20 @@
// 所有接口统一暴露
import * as common from './common'
import * as parking from './parking'
import * as brand from './brand'
import * as car from './car'
import * as member from './member'
import * as comment from './comment'
import * as pact from './pact'
import * as userinfo from './userinfo'
export default {
common,
parking,
brand,
car,
member,
comment,
pact,
userinfo
}

23
src/api/member.js Normal file
View File

@ -0,0 +1,23 @@
// 会员接口
// 修改格式
import qs from 'qs'
import request from '@/utils/request'
// 删除会员
export const memberDelete = (id) => {
return request({
url: `/member/delete?id=${id}`,
method: 'GET'
})
}
// 获取会员详情
export const getMemberDetail = (id) => {
return request({
url: `/member/detail?id=${id}`,
method: 'GET',
})
}

14
src/api/pact.js Normal file
View File

@ -0,0 +1,14 @@
// 合同接口
// 修改格式
import qs from 'qs'
import request from '@/utils/request'
// 查询合同
export const FindPactByPactId = (data) => {
return request({
url: '/FindPactByPactId',
method: 'POST',
data: qs.stringify(data)
})
}

66
src/api/parking.js Normal file
View File

@ -0,0 +1,66 @@
// 停车场接口
// 修改格式
import qs from 'qs'
import request from '@/utils/request'
// 新增停车场
export const parkingAdd = (data) => {
return request({
url: '/parking/add',
method: 'POST',
data: qs.stringify(data)
})
}
// 获取停车场列表
export const getParkingList = (data) => {
return request({
url: '/parking/list',
method: 'POST',
data: qs.stringify(data)
})
}
// 删除停车场
export const deleteParking = (id) => {
return request({
url: `/parking/delete?id=${id}`,
method: 'GET'
})
}
// 获取停车场详情
export const getParkingDetail = (id) => {
return request({
url: `/parking/detail?id=${id}`,
method: 'GET',
})
}
// 编辑停车场
export const editParking = (data) => {
return request({
url: '/parking/edit',
method: 'POST',
data: qs.stringify(data)
})
}
// 修改禁启用状态
export const changeEnable = (id, enable) => {
return request({
url: `/parking/changeEnable?id=${id}&enable=${enable}`,
method: 'GET'
})
}
// 获取停车场
export const getParking = () => {
return request({
url: '/webParking',
method: 'POST',
})
}

51
src/api/user.js Normal file
View File

@ -0,0 +1,51 @@
import request from '@/utils/request'
// 用户注册接口
export function register(data) {
return request({
// url: '/vue-admin-template/user/login',
// url: '/admin/acl/index/login',
url: '/user_register',
method: 'post',
data
})
}
// 发送手机验证码
export function sendcode(data) {
return request({
// url: '/vue-admin-template/user/login',
// url: '/admin/acl/index/login',
url: '/sendCode',
method: 'post',
data
})
}
// 用户登录接口
export function login(data) {
return request({
// url: '/vue-admin-template/user/login',
// url: '/admin/acl/index/login',
url: '/login',
method: 'post',
data
})
}
// 获取个人信息接口
export function getInfo(data) {
return request({
url: '/FindUserById',
method: 'post',
data
})
}
// 用户退出接口
export function logout(token) {
const data = new URLSearchParams();
data.append('token', token);
return request({
// url: '/admin/acl/index/logout',
url: '/logout/',
method: 'post',
data
})
}

23
src/api/userinfo.js Normal file
View File

@ -0,0 +1,23 @@
// 评论接口
// 修改格式
import qs from 'qs'
import request from '@/utils/request'
// 获取个人信息接口
export function getInfo(data) {
return request({
url: '/FindUserById',
method: 'post',
data: qs.stringify(data)
})
}
// 更新个人信息接口
export function RealUser(data) {
return request({
url: '/RealUser',
method: 'post',
data: qs.stringify(data)
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,78 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
<span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script>
import pathToRegexp from 'path-to-regexp'
export default {
data() {
return {
levelList: null
}
},
watch: {
$route() {
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
// only show routes with meta.title
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
const first = matched[0]
if (!this.isDashboard(first)) {
matched = [{ path: '/dashboard', meta: { title: '控制台' }}].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
isDashboard(route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
},
pathCompile(path) {
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
const { params } = this.$route
var toPath = pathToRegexp.compile(path)
return toPath(params)
},
handleLink(item) {
const { redirect, path } = item
if (redirect) {
this.$router.push(redirect)
return
}
this.$router.push(this.pathCompile(path))
}
}
}
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>

View File

@ -0,0 +1,172 @@
<template>
<el-cascader
v-model="regionValue"
:options="region_options"
:props="region_options_props"
:placeholder="initCascaderValue"
@change="changeCascader"
@clearCheckedNodes="clearCheckedNodes"
></el-cascader>
<!-- <div id="app">
<el-cascader
size="large"
:options="options"
v-model="selectedOptions"
@change="handleChange">
</el-cascader>
</div> -->
</template>
<script>
// import { regionData } from 'element-china-area-data';
// import { getCity } from "@/api/common";
// export default {
// data() {
// return {
// options: regionData,
// selectedOptions: [],
// address: [],
// addressData: {},
// initCascaderValue: '',
// };
// },
// methods: {
// handleChange(value) {
// console.log(value);
// },
// async lazyLoad(node, resolve) {
// const { level } = node;
// const requestData = {};
// let levelType = {
// 0: { type: "province" },
// 1: { type: "city", code: "province_code" },
// 2: { type: "area", code: "city_code" },
// };
// if (levelType[level].code) {
// requestData[levelType[level].code] = node.value;
// }
// requestData.type = levelType[level].type;
// let result = await getCity(requestData);
// if (result.code === 200) {
// const data = result.data;
// const type = levelType[level].type;
// data.forEach((item) => {
// item.value = item[type + '_code'];
// item.label = item[type + '_name'];
// item.leaf = level >= 2;
// });
// this.addressData[type] = data;
// resolve(data);
// }
// if (level !== 0) {
// this.getAddress(node);
// }
// },
// changeCascader(value) {
// this.$emit("update:region", value.join());
// const area_code = value[value.length - 1];
// const area_name = this.addressData.area.filter(item => item.value === area_code)[0].label;
// this.address[2] = area_name;
// this.$emit('setMapLocation', this.address.join(""));
// },
// getAddress(node) {
// const index = node.level - 1;
// this.address[index] = node.label;
// },
// clearCheckedNodes() {
// this.selectedOptions = [];
// },
// cascaderValue(value) {
// this.initCascaderValue = value;
// }
// },
// };
import { getCity } from "@/api/common";
export default {
name: "CityArea",
data() {
const _this = this
return {
initCascaderValue: '请选择省市区',
//
address: [],
//
addressData: {},
regionValue: "",
//
region_options: [],
//
region_options_props: {
lazy: true,
async lazyLoad(node, resolve) {
const { level } = node;
//
const requestData = {};
let levelType = {
0: { type: "province" },
1: { type: "city", code: "province_code" },
2: { type: "area", code: "city_code" },
};
if (levelType[level].code) {
requestData[levelType[level].code] = node.value;
}
requestData.type = levelType[level].type;
let result = await getCity(requestData);
if (result.status === 200) {
const data = result.data;
console.log('Address data:', data);
//
const type = levelType[level].type
//
data.forEach((item) => {
item.value = item[type + '_code']
item.label = item[type + '_name']
item.leaf = level >= 2
});
//
_this.addressData[type] = data
resolve(data);
}
//
if (level !== 0) {
_this.getAddress(node)
}
},
},
};
},
props: ["region"],
methods: {
//
changeCascader(value) {
//
this.$emit("update:region", value.join());
//
const area_code = value[value.length - 1]
const area_name = this.addressData.area.filter(item => item.value === area_code)[0].label
this.address[2] = area_name
this.$emit('setMapLocation', this.address.join(""))
},
//
getAddress(node) {
const index = node.level - 1
this.address[index] = node.label
},
//
clearCheckedNodes() {
this.regionValue = ''
},
//
cascaderValue(value) {
this.initCascaderValue = value
}
},
};
</script>
<style>
</style>

View File

@ -0,0 +1,244 @@
<template>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
@close="close"
:close-on-click-modal="false"
:destroy-on-close="true"
>
<el-form :model="form" ref="dialogBrand" :rules="rules">
<el-form-item label="品牌中文" label-width="120px" prop="brand_name_ch">
<el-input v-model="form.brand_name_ch" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="品牌英文" label-width="120px" prop="brand_name_en">
<el-input v-model="form.brand_name_en" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="LOGO" label-width="120px">
<el-upload
:action="uploadURL"
name="img"
:file-list="fileList"
:on-success="uploadSuccess"
:on-change="uploadChange"
list-type="picture-card"
ref="upload"
:auto-upload="false"
:limit="1"
>
<i slot="default" class="el-icon-plus"></i>
<div slot="file" slot-scope="{ file }">
<img class="el-upload-list__item-thumbnail" :src="file.url" />
<span class="el-upload-list__item-actions">
<span
class="el-upload-list__item-preview"
@click="handlePictureCardPreview(file)"
>
<i class="el-icon-zoom-in"></i>
</span>
<span
v-if="!disabled"
class="el-upload-list__item-delete"
@click="handleRemove(file)"
>
<i class="el-icon-delete"></i>
</span>
</span>
</div>
<div slot="tip" class="el-upload__tip">
只能上传jpg/png文件且只能上传一张图片
</div>
</el-upload>
<el-dialog
width="40%"
top="8vh"
:visible.sync="dialogPreviewVisible"
:modal="false"
:close-on-click-modal="false"
>
<img width="100%" :src="dialogImageUrl" alt="" />
</el-dialog>
</el-form-item>
<el-form-item label="类型" label-width="120px" prop="status">
<el-radio-group v-model="form.status">
<el-radio :label="0">禁用</el-radio>
<el-radio :label="1">启用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item class="btn">
<el-button type="primary" @click="submitForm">确定</el-button>
<el-button @click="close">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
</template>
<script>
export default {
name: "AddCarBrand",
props: {
flagVisible: {
type: Boolean,
default: false,
},
},
data() {
return {
//
dialogVisible: false,
//
form: {
brand_name_ch: "",
brand_name_en: "",
status: "",
},
id: "",
//
disabled: false,
dialogPreviewVisible: false,
dialogImageUrl: "",
uploadURL: this.$store.state.app.uploadURL,
fileList: [],
isOldFile: true,
//
rules: {
brand_name_ch: [
{
required: true,
message: "请输入车辆品牌(中文)",
trigger: "blur",
},
],
brand_name_en: [
{
required: true,
message: "请输入车辆品牌(英文)",
trigger: "blur",
},
],
},
};
},
mounted() {
//
this.$bus.$on("brandDetail", (data) => {
this.form.brand_name_ch = data.brand_name_ch;
this.form.brand_name_en = data.brand_name_en;
this.form.status = data.status;
this.id = data.id;
this.fileList.push({ name: data.brand_name_en, url: data.brand_logo });
});
},
methods: {
//
close() {
//
this.$refs.dialogBrand.resetFields();
// upload
this.$refs.upload.clearFiles();
//
this.$emit("update:flagVisible", false);
this.id = "";
this.dialogImageUrl = "";
this.form.brand_name_ch = "";
this.form.brand_name_en = "";
this.form.status = "";
this.fileList = [];
this.isOldFile = true
},
//
submitForm() {
//
this.$refs.dialogBrand.validate(async (valid) => {
if (valid) {
this.upload();
} else {
console.log("error submit!!");
return false;
}
});
},
//
upload() {
if (this.isOldFile && this.fileList.length !== 0 && this.id) {
this.editBrand(this.fileList[0].url)
} else {
this.fileList.length === 0
? this.message("warning", "请上传图片")
: this.$refs.upload.submit();
}
},
//
uploadSuccess(response, file, fileList) {
if (response.code === 200) {
const imgUrl = response.imgUrl;
//
this.id ? this.editBrand(imgUrl) : this.addBrand(imgUrl);
} else {
this.$message.error(response.message);
}
},
//
async addBrand(brand_logo) {
//
let data = JSON.parse(JSON.stringify(this.form));
this.form.status === "" && delete data.status;
data.brand_logo = brand_logo;
let result = await this.$API.brand.addBrand(data);
if (result.code === 200) {
this.message("success", result.message);
this.close();
//
this.$emit("getBrandList");
}
},
//
async editBrand(brand_logo) {
//
let data = JSON.parse(JSON.stringify(this.form));
this.form.status === "" && delete data.status;
data.brand_logo = brand_logo;
data.id = this.id;
let result = await this.$API.brand.editBrand(data);
if (result.code === 200) {
this.message("success", result.message);
this.close();
//
this.$emit("getBrandList");
}
},
//
handleRemove(file) {
this.fileList = [];
this.isOldFile = false
},
//
uploadChange(file, fileList) {
this.fileList = fileList;
},
//
handlePictureCardPreview(file) {
this.dialogPreviewVisible = true;
this.dialogImageUrl = file.url;
}
},
computed: {
title() {
return this.id ? "编辑车辆品牌" : "新增车辆品牌";
},
},
//
watch: {
flagVisible: {
handler(newValue, oldValue) {
this.dialogVisible = newValue;
},
},
},
};
</script>
<style lang="scss" scoped>
.btn {
padding-left: 120px;
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<el-dialog
:title="parkingData.parkingName"
:visible="dialogVisibleMap"
width="70%"
:close-on-click-modal="false"
@close="close"
style="padding: 0"
>
<div class="mapDialog"><Map v-if="dialogVisibleMap" @mapComplete="addMarker" ref="map"></Map></div>
</el-dialog>
</template>
<script>
import Map from '@/views/map/map'
export default {
name: 'mapDiglog',
components: { Map },
props: ['dialogVisibleMap', 'parkingData'],
methods: {
close() {
this.$emit('update:dialogVisibleMap', false)
},
//
addMarker() {
this.$refs.map.addMarker(this.parkingData.coord)
}
},
};
</script>
<style lang="scss" scoped>
.mapDialog {
width: 100%;
height: 650px;
}
</style>

View File

@ -0,0 +1,110 @@
<template>
<div>
<el-form ref="form" :model="formData" :label-width="config.form_config.label_width" :inline="config.form_config.inline">
<el-form-item v-for="item in config.form_item_config" :key="item.prop" :rules="item.rules" :label="item.label" :prop="item.prop">
<!-- 输入框类型 -->
<el-input v-if="item.type === 'input'" v-model.trim="formData[item.prop]" :placeholder="item.placeholder" :disabled="item.disabled" :style="{ width: item.width}"></el-input>
<!-- 插槽类型 -->
<slot v-if="item.type === 'slot'" :name="item.slotName"></slot>
<!-- 单选类型 -->
<el-radio-group v-if="item.type === 'radio'" v-model="formData[item.prop]">
<el-radio v-for="radio in item.options" :key="radio.value" :label="radio.value">{{ radio.label }}</el-radio>
</el-radio-group>
<!-- 开关类型 -->
<el-switch v-if="item.type === 'switch'" v-model="formData[item.prop]" active-color="#13ce66"></el-switch>
<!-- 下拉框类型 -->
<el-select v-if="item.type === 'select'" v-model="formData[item.prop]" :placeholder="item.placeholder">
<el-option v-for="option in item.options" :key="option.id || option.value" :label="option.label" :value="option.value">
</el-option>
</el-select>
<!-- 富文本编辑器 -->
<div v-if="item.type === 'richText'" ref="editorDom"></div>
</el-form-item>
<!-- 按钮 -->
<el-form-item>
<el-button v-for="item in config.form_handler" :key="item.key" :type="item.type" @click="item.handler && item.handler()">
{{item.label}}
</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import E from "wangeditor";
export default {
name: 'Form',
props: ['config', 'formData'],
data() {
return {
required_msg: {
'input': '请输入',
'radio': '请选择',
'select': '请选择'
},
//
editor: null,
}
},
mounted() {
this.config.form_item_config.forEach(item => {
if (item.type === 'richText') {
this.createEditor()
return false
}
})
},
methods: {
// form
initForm() {
this.config.form_item_config.forEach(item => {
// rules
if (item.required) this.rules(item);
})
},
rules(item) {
const requiredRules = [{ required: true, message: item.required_msg || `${this.required_msg[item.type]}${item.label}`, trigger: "change" }]
//
if (item.rules && item.rules.length > 0) {
item.rules = requiredRules.concat(item.rules)
} else {
item.rules = requiredRules
}
},
//
createEditor() {
this.editor = new E(this.$refs.editorDom);
this.editor.create();
},
//
clearText() {
this.editor.txt.clear()
}
},
watch: {
config: {
immediate: true,
handler(newValue) {
this.initForm()
}
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,44 @@
<template>
<div style="padding: 0 15px;" @click="toggleClick">
<svg
:class="{'is-active':isActive}"
class="hamburger"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
>
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
</svg>
</div>
</template>
<script>
export default {
name: 'Hamburger',
props: {
isActive: {
type: Boolean,
default: false
}
},
methods: {
toggleClick() {
this.$emit('toggleClick')
}
}
}
</script>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>

View File

@ -0,0 +1,91 @@
<template>
<ul :class="{ displayShow: displayShow, nav: true }">
<li>
<i class="el-icon-warning-outline"></i>
</li>
<li>
<i class="el-icon-search"></i>
</li>
<li class="choose-car" @click="chooseCar">选择车辆</li>
<li @click="moveTolocation">
<i class="el-icon-location-outline"></i>
</li>
<!-- <li @click="goToUser">
<i class="el-icon-user"></i>
</li> -->
</ul>
</template>
<script>
export default {
name: "Nav",
methods: {
//
goToUser() {
this.$router.push({
name: this.$store.state.userAbout.token ? "User" : "Login"
});
},
//
chooseCar() {
localStorage.getItem('id') ? this.$router.push({ name: "Car" }) : this.$message({type: 'warning', message: '请先选择停车场'})
},
//
moveTolocation() {
this.$bus.$emit("location");
},
},
computed: {
displayShow() {
return this.$route.name === "Car" ? true : false;
},
},
};
</script>
<style lang="scss" scoped>
.nav {
position: fixed;
bottom: 86px;
left: 50%;
transform: translate(-50%);
z-index: 1;
height: 86px;
line-height: 86px;
color: #fff;
li {
width: 44px;
height: 44px;
line-height: 44px;
text-align: center;
background-color: #3c4147;
border-radius: 50%;
display: inline-block;
margin-right: 34px;
&:hover {
cursor: pointer;
color: #00a3ff;
}
i {
line-height: 44px;
font-size: 16px;
&:hover {
color: #00a3ff;
}
}
}
.choose-car {
width: 240px;
height: 100%;
line-height: 86px;
margin-right: 34px;
font-size: 26px;
border-radius: 40px;
}
}
.displayShow {
display: none;
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<el-pagination
:page-sizes="[10, 17, 26, 35]"
:page-size="pageSize"
:current-page="pageNumber"
layout="->, total, sizes, prev, pager, next, jumper"
background
class="pagination"
@size-change="sizeChange"
@current-change="currentChange"
:total="total">
</el-pagination>
</template>
<script>
export default {
name: 'Pagination',
props: ['total', 'pageSize', 'pageNumber'],
methods: {
sizeChange(value) {
this.$emit('update:pageSize', value)
this.$emit('getDataList')
},
currentChange(value) {
this.$emit('update:pageNumber', value)
this.$emit('getDataList')
}
},
}
</script>
<style lang="scss" scoped>
.pagination {
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,62 @@
<template>
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :xlink:href="iconName" />
</svg>
</template>
<script>
// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
import { isExternal } from '@/utils/validate'
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
computed: {
isExternal() {
return isExternal(this.iconClass)
},
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon() {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
}
}
}
}
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
}
</style>

View File

@ -0,0 +1,181 @@
<template>
<div>
<el-table
:data="tableList"
tooltip-effect="dark"
style="width: 100%"
border
v-loading="table_loading"
element-loading-text="正在加载中..."
class="table"
>
<!-- 勾选列 -->
<el-table-column
v-if="config.checkedColumn"
type="selection"
width="40"
></el-table-column>
<template v-for="config_header in config.table_headers">
<!-- 需要回调处理值的列 -->
<el-table-column
v-if="config_header.type === 'function'"
:key="config_header.prop"
:prop="config_header.prop"
:label="config_header.label"
:width="config_header.width"
:align="config_header.align || 'left'"
>
<template slot-scope="scope">
<span>{{
config_header.callback && config_header.callback(scope.row)
}}</span>
</template>
</el-table-column>
<!-- 需要放入其他html标签例如buttonimg使用插槽处理-->
<el-table-column
v-else-if="config_header.type === 'slot'"
:key="config_header.prop"
:prop="config_header.prop"
:label="config_header.label"
:width="config_header.width"
:align="config_header.align"
>
<template slot-scope="scope">
<slot :name="config_header.slotName" :data="scope.row"></slot>
</template>
</el-table-column>
<!-- 纯文本 -->
<el-table-column
v-else
:key="config_header.prop"
:prop="config_header.prop"
:label="config_header.label"
:width="config_header.width"
:align="config_header.align"
></el-table-column>
</template>
</el-table>
<!-- 分页器 -->
<el-pagination
v-if="config.pagination"
:page-sizes="[10, 17, 26, 35]"
:page-size="pageSize"
:current-page="pageNumber"
layout="->, total, sizes, prev, pager, next, jumper"
background
class="pagination"
@size-change="sizeChange"
@current-change="currentChange"
:total="total"
>
</el-pagination>
</div>
</template>
<script>
export default {
name: "Table",
props: ["table_config"],
data() {
return {
//
config: {
//
table_headers: [],
// API
axios_config: {},
//
requestData: {},
//
checkedColumn: true,
//
pagination: true,
},
//
tableList: [],
pageSize: 10,
pageNumber: 1,
total: 0,
//
table_loading: false,
};
},
mounted() {
this.initConfig();
},
methods: {
//
initConfig() {
for (const key in this.table_config) {
if (Object.keys(this.config).includes(key)) {
this.config[key] = this.table_config[key];
}
}
//
this.getTableData();
},
//
async getTableData() {
let { url, method } = this.config.axios_config;
let reqData = {
url,
method,
data: this.config.requestData,
};
this.table_loading = true;
let result = await this.$API.common.tableData(reqData);
if (result.status === 200) {
this.tableList = result.data;
// DOM
this.$nextTick(() => {
// todo something...
});
this.total = result.total;
this.table_loading = false;
} else {
this.tableList = []
this.$message({
type: 'warning',
message: result.message
})
this.table_loading = false;
}
},
//
getTableDataWithParams(params = "") {
if (params) {
//
this.config.requestData = params;
}
this.getTableData();
},
//
sizeChange(value) {
this.config.requestData.pageSize = value;
this.getTableData();
},
//
currentChange(value) {
this.config.requestData.pageNumber = value;
this.getTableData();
},
},
watch: {
table_config: {
immediate: true,
handler(newValue) {
// console.log(newValue);
},
},
},
};
</script>
<style lang="scss" scoped>
.table {
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,47 @@
<template>
<div style="display: flex; flex-direction: column; justify-content: center">
<input
type="file"
@change="uploadImage"
ref="fileInput"
style="display: none"
/>
<el-button type="success" @click="openFileInput">上传照片</el-button>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "Upload",
data() {
return {
photo: "",
};
},
methods: {
openFileInput() {
this.$refs.fileInput.click();
},
uploadImage(event) {
var file = event.target.files[0];
var formData = new FormData();
formData.append("image", file);
formData.append("token", "1c17b11693cb5ec63859b091c5b9c1b2");
axios
.post("http://8.141.3.231:40061/api/index.php", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
})
.then((response) => {
this.$emit("upload-success", response.data.url);
this.$message({ type: "success", message: "上传成功" });
})
.catch((error) => {
this.$message.error("上传图片失败:" + error);
});
},
},
};
</script>

9
src/icons/index.js Normal file
View File

@ -0,0 +1,9 @@
import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon'// svg component
// register globally
Vue.component('svg-icon', SvgIcon)
const req = require.context('./svg', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys().map(requireContext)
requireAll(req)

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#e6e6e6" d="M224.304762 497.371429h175.542857v78.019047H224.304762zM872.838095 258.438095l-97.523809-146.285714c-9.752381-14.628571-24.380952-24.380952-39.009524-24.380952H258.438095L146.285714 258.438095H4.87619v97.52381h78.019048L19.504762 448.609524c-4.87619 9.752381-4.87619 19.504762-4.876191 29.257143v385.219047H341.333333v-117.028571h204.8v-97.52381H243.809524v117.028572H107.27619v-273.066667l53.638096-78.019048h697.295238l39.009524 58.514286 82.895238-53.638095-39.009524-58.514286H1024v-97.523809h-151.161905zM229.180952 316.952381l82.895238-126.780952h399.84762l82.895238 126.780952H229.180952zM980.114286 551.009524l-63.390476 34.133333c-19.504762-24.380952-48.761905-39.009524-82.895239-48.761905V463.238095h-78.019047V536.380952c-29.257143 9.752381-58.514286 24.380952-82.895238 48.761905l-63.390476-39.009524v4.876191l-39.009524 68.266666 68.266666 39.009524c-4.87619 9.752381-4.87619 24.380952-4.87619 39.009524 0 14.628571 4.87619 29.257143 4.87619 43.885714l-68.266666 39.009524h-4.876191l39.009524 68.266667 68.266667-39.009524c19.504762 24.380952 48.761905 39.009524 78.019047 48.761905v78.019047H828.952381v-78.019047c29.257143-9.752381 58.514286-24.380952 78.019048-43.885714l68.266666 39.009523 39.009524-68.266666-68.266667-39.009524c4.87619-14.628571 9.752381-29.257143 9.752381-48.761905 0-14.628571-4.87619-29.257143-4.87619-43.885714l63.390476-39.009524-34.133333-63.390476z m-185.295238 234.057143c-48.761905 0-87.771429-39.009524-87.771429-87.771429s39.009524-87.771429 87.771429-87.771428 87.771429 39.009524 87.771428 87.771428-39.009524 87.771429-87.771428 87.771429z" /></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1 @@
<svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg>

After

Width:  |  Height:  |  Size: 497 B

View File

@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
src/icons/svg/eye.svg Normal file
View File

@ -0,0 +1 @@
<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>

After

Width:  |  Height:  |  Size: 944 B

1
src/icons/svg/form.svg Normal file
View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.068 23.784c-1.02 0-1.877-.32-2.572-.96a8.588 8.588 0 0 1-1.738-2.237 11.524 11.524 0 0 1-1.042-2.621c-.232-.895-.348-1.641-.348-2.238V0h.278c.834 0 1.622.085 2.363.256.742.17 1.645.575 2.711 1.214 1.066.64 2.363 1.535 3.892 2.686 1.53 1.15 3.453 2.664 5.77 4.54 2.502 2.045 4.494 3.771 5.977 5.178 1.483 1.406 2.618 2.6 3.406 3.58.787.98 1.274 1.812 1.46 2.494.185.682.277 1.278.277 1.79v2.046H84.068zM127.3 84.01c.278.682.464 1.535.556 2.558.093 1.023-.37 2.003-1.39 2.94-.463.427-.88.832-1.25 1.215-.372.384-.696.704-.974.96a6.69 6.69 0 0 1-.973.767l-11.816-10.741a44.331 44.331 0 0 0 1.877-1.535 31.028 31.028 0 0 1 1.737-1.406c1.112-.938 2.317-1.343 3.615-1.215 1.297.128 2.363.405 3.197.83.927.427 1.923 1.173 2.989 2.239 1.065 1.065 1.876 2.195 2.432 3.388zM78.23 95.902c2.038 0 3.752-.511 5.143-1.534l-26.969 25.83H18.037c-1.761 0-3.684-.47-5.77-1.407a24.549 24.549 0 0 1-5.838-3.709 21.373 21.373 0 0 1-4.518-5.306c-1.204-2.003-1.807-4.07-1.807-6.202V16.495c0-1.79.44-3.665 1.32-5.626A18.41 18.41 0 0 1 5.04 5.562a21.798 21.798 0 0 1 5.213-3.964C12.198.533 14.237 0 16.37 0h53.24v15.984c0 1.62.278 3.367.834 5.242a16.704 16.704 0 0 0 2.572 5.179c1.159 1.577 2.665 2.898 4.518 3.964 1.853 1.066 4.078 1.598 6.673 1.598h20.295v42.325L85.458 92.45c1.02-1.364 1.529-2.856 1.529-4.476 0-2.216-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1c-2.409 0-4.448.789-6.116 2.366-1.668 1.577-2.502 3.474-2.502 5.69 0 2.217.834 4.092 2.502 5.626 1.668 1.535 3.707 2.302 6.117 2.302h52.13zM26.1 47.951c-2.41 0-4.449.789-6.117 2.366-1.668 1.577-2.502 3.473-2.502 5.69 0 2.216.834 4.092 2.502 5.626 1.668 1.534 3.707 2.302 6.117 2.302h52.13c2.409 0 4.47-.768 6.185-2.302 1.715-1.534 2.572-3.41 2.572-5.626 0-2.217-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1zm52.407 64.063l1.807-1.663 3.476-3.196a479.75 479.75 0 0 0 4.587-4.284 500.757 500.757 0 0 1 5.004-4.667c3.985-3.666 8.48-7.758 13.485-12.276l11.677 10.741-13.485 12.404-5.004 4.603-4.587 4.22a179.46 179.46 0 0 0-3.267 3.068c-.88.853-1.367 1.322-1.46 1.407-.463.341-.973.703-1.529 1.087-.556.383-1.112.703-1.668.959-.556.256-1.413.575-2.572.959a83.5 83.5 0 0 1-3.545 1.087 72.2 72.2 0 0 1-3.475.895c-1.112.256-1.946.426-2.502.511-1.112.17-1.854.043-2.224-.383-.371-.426-.464-1.151-.278-2.174.092-.511.278-1.279.556-2.302.278-1.023.602-2.067.973-3.132l1.042-3.005c.325-.938.58-1.577.765-1.918a10.157 10.157 0 0 1 2.224-2.941z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

1
src/icons/svg/link.svg Normal file
View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></svg>

After

Width:  |  Height:  |  Size: 285 B

1
src/icons/svg/member.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1675579475154" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2447" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M1007.4 597.1l-50 0c-4.6-16.5-11.1-32.2-19.3-46.8l35.3-35.3c6.5-6.5 6.5-17 0-23.5l-57-57c-3.2-3.2-7.5-4.8-11.7-4.8-4.3 0-8.5 1.6-11.7 4.8l-35.3 35.3c-14.6-8.2-30.3-14.7-46.8-19.3l0-50c0-9.1-7.5-16.6-16.6-16.6l-80.6 0c-9.1 0-16.6 7.5-16.6 16.6l0 50c-16.5 4.6-32.2 11.1-46.8 19.3L615 434.6c-3.2-3.2-7.5-4.8-11.7-4.8s-8.5 1.6-11.7 4.8l-57 57c-6.4 6.5-6.4 17 0 23.5l35.3 35.3c-8.2 14.6-14.7 30.3-19.3 46.8l-50 0c-9.1 0-16.6 7.5-16.6 16.6l0 80.6c0 9.1 7.5 16.6 16.6 16.6l50 0c4.6 16.5 11.1 32.2 19.3 46.8L534.5 793c-6.4 6.5-6.4 17 0 23.5l57 57c3.2 3.2 7.5 4.8 11.7 4.8s8.5-1.6 11.7-4.8l35.3-35.3c14.6 8.2 30.3 14.7 46.8 19.3l0 50c0 9.1 7.5 16.6 16.6 16.6l80.6 0c9.1 0 16.6-7.5 16.6-16.6l0-50c16.5-4.6 32.2-11.1 46.8-19.3l35.3 35.3c3.2 3.2 7.5 4.8 11.7 4.8 4.3 0 8.5-1.6 11.7-4.8l57-57c6.5-6.5 6.5-17 0-23.5l-35.3-35.3c8.2-14.6 14.7-30.3 19.3-46.8l50 0c9.1 0 16.6-7.5 16.6-16.6l0-80.6C1024 604.6 1016.5 597.1 1007.4 597.1zM895.8 693.8c-3.2 11.5-7.7 22.5-13.4 32.6l-20.3 35.9-35.9 20.3c-10.1 5.7-21 10.2-32.6 13.4L753.9 807l-39.7-11.1c-11.5-3.2-22.5-7.7-32.6-13.4l-35.9-20.3-20.3-35.9c-5.7-10.1-10.2-21-13.4-32.6L601 654.1l11.1-39.7c3.2-11.5 7.7-22.5 13.4-32.6l20.3-35.9 35.9-20.3c10.1-5.7 21-10.2 32.6-13.4l39.7-11.1 39.7 11.1c11.5 3.2 22.5 7.7 32.6 13.4l35.9 20.3 20.3 35.9c5.7 10.1 10.2 21 13.4 32.6l11.1 39.7L895.8 693.8z" p-id="2448"></path><path d="M753.9 552.8c-55.9 0-101.3 45.3-101.3 101.3 0 55.9 45.3 101.3 101.3 101.3 55.9 0 101.3-45.3 101.3-101.3C855.2 598.1 809.9 552.8 753.9 552.8zM753.9 691.3c-20.6 0-37.3-16.7-37.3-37.3s16.7-37.3 37.3-37.3 37.3 16.7 37.3 37.3S774.5 691.3 753.9 691.3z" p-id="2449"></path><path d="M672 923.5c-17.7 0-32 14.3-32 32l0 4.5L64 960 64 798.5c0-157.5 123.2-283.9 280.5-287.9 5-0.1 10-0.1 14.9 0 37.1 0.9 73 8.8 106.8 23.5 16.2 7 35.1-0.4 42.1-16.7 7-16.2-0.4-35.1-16.7-42.1-4.8-2.1-9.6-4-14.4-5.9 75.4-43.4 126.2-124.8 126.2-218C603.4 112.6 490.9 0 352 0S100.6 112.6 100.6 251.4c0 93.2 50.7 174.6 126.1 218-47.2 17.9-90.3 46.1-126.5 83.2C35.6 618.7 0 706.1 0 798.5l0 169.7C0 999 25 1024 55.7 1024l592.5 0c30.7 0 55.7-25 55.7-55.7l0-12.8C704 937.8 689.7 923.5 672 923.5zM164.6 251.4c0-50.1 19.5-97.1 54.9-132.5C254.9 83.5 301.9 64 352 64c50.1 0 97.1 19.5 132.5 54.9 35.4 35.4 54.9 82.5 54.9 132.5 0 50.1-19.5 97.1-54.9 132.5-35.4 35.4-82.5 54.9-132.5 54.9-50.1 0-97.1-19.5-132.5-54.9C184.1 348.6 164.6 301.5 164.6 251.4z" p-id="2450"></path></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

1
src/icons/svg/nested.svg Normal file
View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.002 9.2c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-5.043-3.58-9.132-7.997-9.132S.002 4.157.002 9.2zM31.997.066h95.981V18.33H31.997V.066zm0 45.669c0 5.044 3.58 9.132 7.998 9.132 4.417 0 7.997-4.088 7.997-9.132 0-3.263-1.524-6.278-3.998-7.91-2.475-1.63-5.524-1.63-7.998 0-2.475 1.632-4 4.647-4 7.91zM63.992 36.6h63.986v18.265H63.992V36.6zm-31.995 82.2c0 5.043 3.58 9.132 7.998 9.132 4.417 0 7.997-4.089 7.997-9.132 0-5.044-3.58-9.133-7.997-9.133s-7.998 4.089-7.998 9.133zm31.995-9.131h63.986v18.265H63.992V109.67zm0-27.404c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-3.263-1.524-6.277-3.998-7.909-2.475-1.631-5.524-1.631-7.998 0-2.475 1.632-4 4.646-4 7.91zm31.995-9.13h31.991V91.4H95.987V73.135z"/></svg>

After

Width:  |  Height:  |  Size: 821 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="198.26px" viewBox="0 0 1033 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#e6e6e6" d="M672 275.2C672 121.6 547.2 0 393.6 0H0v1008h140.8V550.4h252.8c153.6 0 278.4-124.8 278.4-275.2z m-300.8 131.2H140.8V140.8h230.4c67.2 3.2 128 64 128 131.2 3.2 67.2-60.8 131.2-128 134.4zM1017.6 761.6c-3.2-9.6-16-19.2-16-19.2L950.4 608c-9.6-25.6-41.6-38.4-54.4-38.4h-265.6c-12.8 0-44.8 12.8-54.4 38.4l-51.2 137.6s-12.8 9.6-16 19.2c-3.2 9.6-9.6 16-9.6 76.8 0 57.6 12.8 80 25.6 86.4l9.6 6.4v54.4c0 19.2 3.2 22.4 22.4 22.4h38.4c16 0 28.8 0 28.8-19.2v-54.4h284.8V992c0 19.2 12.8 19.2 28.8 19.2h38.4c16 0 22.4-6.4 22.4-22.4v-54.4l9.6-6.4c12.8-6.4 25.6-28.8 25.6-86.4-6.4-64-9.6-70.4-16-80z m-428.8-35.2c3.2-6.4 16-67.2 22.4-89.6 3.2-6.4 19.2-28.8 38.4-28.8h220.8c19.2 0 35.2 19.2 38.4 28.8 6.4 22.4 19.2 80 22.4 89.6 6.4 9.6-3.2 9.6-3.2 9.6H595.2s-6.4 0-6.4-9.6z m6.4 147.2c-22.4 0-41.6-19.2-41.6-41.6 0-22.4 19.2-41.6 41.6-41.6 22.4 0 41.6 19.2 41.6 41.6 0 25.6-19.2 41.6-41.6 41.6z m336 0c-22.4 0-41.6-19.2-41.6-41.6 0-22.4 19.2-41.6 41.6-41.6 22.4 0 41.6 19.2 41.6 41.6-3.2 25.6-19.2 41.6-41.6 41.6z" /></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg>

After

Width:  |  Height:  |  Size: 623 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#e6e6e6" d="M512 1011.3c-16.2 0-32.4-4.2-46.8-12.5L113.8 795.9C85 779.2 67 748.2 67 714.9V309.1c0-33.3 17.9-64.4 46.8-81L465.2 25.2c28.9-16.7 64.7-16.7 93.6 0l351.4 202.9c28.9 16.7 46.8 47.7 46.8 81V636c0 17.7-14.3 32-32 32s-32-14.3-32-32V309.1c0-10.5-5.7-20.3-14.8-25.6L526.8 80.6c-9.1-5.3-20.4-5.3-29.6 0L145.8 283.5c-9.1 5.3-14.8 15.1-14.8 25.6v405.7c0 10.5 5.7 20.3 14.8 25.6l351.4 202.9c9.1 5.3 20.4 5.3 29.6 0l320.7-185.2c15.3-8.8 34.9-3.6 43.7 11.7 8.8 15.3 3.6 34.9-11.7 43.7L558.8 998.8c-14.4 8.3-30.6 12.5-46.8 12.5z" /><path fill="#e6e6e6" d="M573.6 544.5c-5.8 0-11.6-1.4-17-4.3L511 515.8l-45.9 23.7c-12.3 6.3-26.8 5.2-37.9-3-11.1-8.2-16.5-21.7-14.1-35.3l9.1-50.9-36.7-36.4c-9.8-9.7-13.2-23.9-8.8-37s15.6-22.4 29.2-24.3l51.2-7 23.2-46.2c6.2-12.2 18.5-19.8 32.2-19.8h0.3c13.8 0.1 26.1 7.9 32.1 20.3l22.5 46.5 51.1 7.8c13.6 2.1 24.7 11.6 28.9 24.7 4.2 13.2 0.6 27.3-9.4 36.8l-37.3 35.8 8.4 51c2.2 13.6-3.4 27.1-14.6 35.1-6.2 4.6-13.5 6.9-20.9 6.9z m13.2-60.8z m-75.5-35.6c5.8 0 11.7 1.4 17 4.3l8.5 4.6-1.6-9.5c-1.9-11.7 2-23.6 10.6-31.8l7-6.7-9.6-1.5c-11.7-1.8-21.8-9.2-27-19.9l-4.2-8.7-4.3 8.6c-5.3 10.6-15.5 17.9-27.3 19.5l-9.6 1.3 6.9 6.8c8.4 8.3 12.2 20.3 10.1 31.9l-1.7 9.5 8.6-4.4c5.3-2.6 10.9-4 16.6-4z" /><path fill="#e6e6e6" d="M512 220c51.3 0 99.5 20 135.8 56.2S704 360.7 704 412c0 35.6-9.8 70.4-28.4 100.5l-20 32.5 19.1 33.1 54.3 94-12-0.6c-1.1-0.1-2.3-0.1-3.5-0.1-5.7 0-11.3 0.7-16.8 2.1-14.5 3.7-27.3 11.7-37 23.1l-4.8 5.6-3.4 6.6-5.7 11.2-49.5-85.7-20.9-36.3-41.6 4.7c-7.3 0.8-14.6 1.2-21.8 1.2s-14.5-0.4-21.8-1.2l-41.6-4.7-20.9 36.3-49.5 85.7-5.7-11.2-3.4-6.6-4.8-5.6c-9.7-11.4-22.5-19.4-37-23.1-5.4-1.4-11.1-2.1-16.8-2.1-1.1 0-2.3 0-3.5 0.1l-12 0.6 54.3-94 19.1-33.1-20-32.5C329.8 482.4 320 447.6 320 412c0-51.3 20-99.5 56.2-135.8S460.7 220 512 220m0-64c-141.4 0-256 114.6-256 256 0 49.2 13.9 95.1 37.9 134.1l-86 148.9c-10.4 18 0.2 40.8 20.6 44.5l81.7-4.1h0.3c0.3 0 0.6 0 0.9 0.1 1.6 0.4 3 1.3 4.1 2.6l37.6 73.3c6.1 7.1 14.5 10.6 22.8 10.6 10.2 0 20.3-5.1 26-15l81.2-140.6c9.5 1.1 19.1 1.6 28.9 1.6s19.4-0.6 28.9-1.6L622.1 807c5.7 9.9 15.8 15 26 15 8.4 0 16.8-3.5 22.8-10.6l37.6-73.3c1.1-1.3 2.5-2.2 4.1-2.6 0.3-0.1 0.6-0.1 0.9-0.1h0.3l81.7 4.1c20.4-3.7 31-26.5 20.6-44.5l-86-148.9c24-39 37.9-84.9 37.9-134.1 0-141.4-114.6-256-256-256z" /></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

1
src/icons/svg/table.svg Normal file
View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg>

After

Width:  |  Height:  |  Size: 597 B

1
src/icons/svg/tree.svg Normal file
View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

1
src/icons/svg/user.svg Normal file
View File

@ -0,0 +1 @@
<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>

After

Width:  |  Height:  |  Size: 440 B

22
src/icons/svgo.yml Normal file
View File

@ -0,0 +1,22 @@
# replace default config
# multipass: true
# full: true
plugins:
# - name
#
# or:
# - name: false
# - name: true
#
# or:
# - name:
# param1: 1
# param2: 2
- removeAttrs:
attrs:
- 'fill'
- 'fill-rule'

View File

@ -0,0 +1,40 @@
<template>
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<router-view :key="key" />
</transition>
</section>
</template>
<script>
export default {
name: 'AppMain',
computed: {
key() {
return this.$route.path
}
}
}
</script>
<style scoped>
.app-main {
/*50 = navbar */
min-height: calc(100vh - 50px);
width: 100%;
position: relative;
overflow: hidden;
}
.fixed-header+.app-main {
padding-top: 50px;
}
</style>
<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
.fixed-header {
padding-right: 15px;
}
}
</style>

View File

@ -0,0 +1,167 @@
<template>
<div class="navbar">
<hamburger
:is-active="sidebar.opened"
class="hamburger-container"
@toggleClick="toggleSideBar"
/>
<breadcrumb class="breadcrumb-container" />
<div
class="user_info"
style="
position: absolute;
height: 100%;
line-height: 50px;
right: 110px;
display: flex;
align-items: center;
"
>
<img
:src="avatar"
class="user-avatar"
style="width: 40px; height: 40px; border-radius: 50%"
@click="gotoUserCenter"
/>
<div style="margin-left: 15px; line-height: 50px">{{ userName }}</div>
</div>
<div class="right-menu">
<div class="logout" @click="logout">
<i class="el-icon-switch-button"></i>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapState } from "vuex";
import Breadcrumb from "@/components/Breadcrumb";
import Hamburger from "@/components/Hamburger";
export default {
components: {
Breadcrumb,
Hamburger,
},
data() {
return {
avatar: "",
};
},
created() {
this.avatar =
this.$store.state.user.avatar != ""
? this.$store.state.user.avatar
: window.localStorage.getItem("avatar");
},
computed: {
userName() {
return window.localStorage.getItem("userName");
},
...mapGetters(["sidebar", "avatar", "name"]),
},
methods: {
gotoUserCenter() {
this.$router.push("/memberManage/memberList");
},
toggleSideBar() {
this.$store.dispatch("app/toggleSideBar");
},
logout() {
this.$confirm("是否退出登录?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
await this.$store.dispatch("user/logout");
this.$router.push(`/login?redirect=${this.$route.fullPath}`);
})
.catch(() => {});
return false;
// await this.$store.dispatch("user/logout");
// this.$router.push(`/login?redirect=${this.$route.fullPath}`);
},
},
};
</script>
<style lang="scss" scoped>
.navbar {
height: 50px;
overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
cursor: pointer;
transition: background 0.3s;
-webkit-tap-highlight-color: transparent;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
.breadcrumb-container {
float: left;
}
.right-menu {
float: right;
height: 100%;
line-height: 50px;
.logout {
width: 80px;
height: 50px;
border-left: 2px solid #eee;
box-sizing: border-box;
font-size: 25px;
text-align: center;
cursor: pointer;
&:hover {
color: #5cb6ff;
}
}
.user_info {
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
}
}
.avatar-container {
margin-right: 30px;
.avatar-wrapper {
margin-top: 5px;
position: relative;
.user-avatar {
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 10px;
}
.el-icon-caret-bottom {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
}
}
}
</style>

View 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)
}
}
}
}
}

View File

@ -0,0 +1,41 @@
<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) {
if (icon.includes('el-icon')) {
vnodes.push(<i class={[icon, 'sub-el-icon']} />)
} else {
vnodes.push(<svg-icon icon-class={icon}/>)
}
}
if (title) {
vnodes.push(<span slot='title'>{(title)}</span>)
}
return vnodes
}
}
</script>
<style scoped>
.sub-el-icon {
color: currentColor;
width: 1em;
height: 1em;
}
</style>

View 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>

View File

@ -0,0 +1,82 @@
<template>
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
<transition name="sidebarLogoFade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo">
<h1 v-else class="sidebar-title">{{ title }} </h1>
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo">
<h1 class="sidebar-title">{{ title }} </h1>
</router-link>
</transition>
</div>
</template>
<script>
export default {
name: 'SidebarLogo',
props: {
collapse: {
type: Boolean,
required: true
}
},
data() {
return {
title: 'Vue Admin Template',
logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png'
}
}
}
</script>
<style lang="scss" scoped>
.sidebarLogoFade-enter-active {
transition: opacity 1.5s;
}
.sidebarLogoFade-enter,
.sidebarLogoFade-leave-to {
opacity: 0;
}
.sidebar-logo-container {
position: relative;
width: 100%;
height: 50px;
line-height: 50px;
background: #2b2f3a;
text-align: center;
overflow: hidden;
& .sidebar-logo-link {
height: 100%;
width: 100%;
& .sidebar-logo {
width: 32px;
height: 32px;
vertical-align: middle;
margin-right: 12px;
}
& .sidebar-title {
display: inline-block;
margin: 0;
color: #fff;
font-weight: 600;
line-height: 50px;
font-size: 14px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
vertical-align: middle;
}
}
&.collapse {
.sidebar-logo {
margin-right: 0px;
}
}
}
</style>

View File

@ -0,0 +1,95 @@
<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-noDropdown':!isNest}">
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
</el-menu-item>
</app-link>
</template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="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>

View File

@ -0,0 +1,56 @@
<template>
<div :class="{'has-logo':showLogo}">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
export default {
components: { SidebarItem, Logo },
computed: {
...mapGetters([
'sidebar'
]),
routes() {
return this.$router.options.routes
},
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
}
}
}
</script>

View File

@ -0,0 +1,3 @@
export { default as Navbar } from './Navbar'
export { default as Sidebar } from './Sidebar'
export { default as AppMain } from './AppMain'

93
src/layout/index.vue Normal file
View File

@ -0,0 +1,93 @@
<template>
<div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<sidebar class="sidebar-container" />
<div class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<navbar />
</div>
<app-main />
</div>
</div>
</template>
<script>
import { Navbar, Sidebar, AppMain } from './components'
import ResizeMixin from './mixin/ResizeHandler'
export default {
name: 'Layout',
components: {
Navbar,
Sidebar,
AppMain
},
mixins: [ResizeMixin],
computed: {
sidebar() {
return this.$store.state.app.sidebar
},
device() {
return this.$store.state.app.device
},
fixedHeader() {
return this.$store.state.settings.fixedHeader
},
classObj() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile'
}
}
},
methods: {
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/styles/mixin.scss";
@import "~@/styles/variables.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar{
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$sideBarWidth});
transition: width 0.28s;
}
.hideSidebar .fixed-header {
width: calc(100% - 54px)
}
.mobile .fixed-header {
width: 100%;
}
</style>

View File

@ -0,0 +1,45 @@
import store from '@/store'
const { body } = document
const WIDTH = 992 // refer to Bootstrap's responsive design
export default {
watch: {
$route(route) {
if (this.device === 'mobile' && this.sidebar.opened) {
store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
},
beforeMount() {
window.addEventListener('resize', this.$_resizeHandler)
},
beforeDestroy() {
window.removeEventListener('resize', this.$_resizeHandler)
},
mounted() {
const isMobile = this.$_isMobile()
if (isMobile) {
store.dispatch('app/toggleDevice', 'mobile')
store.dispatch('app/closeSideBar', { withoutAnimation: true })
}
},
methods: {
// use $_ for mixins properties
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
$_isMobile() {
const rect = body.getBoundingClientRect()
return rect.width - 1 < WIDTH
},
$_resizeHandler() {
if (!document.hidden) {
const isMobile = this.$_isMobile()
store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
if (isMobile) {
store.dispatch('app/closeSideBar', { withoutAnimation: true })
}
}
}
}
}

79
src/main.js Normal file
View File

@ -0,0 +1,79 @@
import Vue from 'vue'
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import locale from 'element-ui/lib/locale/lang/en' // lang i18n
import '@/styles/index.scss' // global css
import App from './App'
import store from './store'
import router from './router'
import '@/icons' // icon
import '@/permission' // permission control
/**
* If you don't want to use mock-server
* you want to use MockJs for mock api
* you can execute: mockXHR()
*
* Currently MockJs will be used in the production environment,
* please remove it before going online ! ! !
*/
if (process.env.NODE_ENV === 'production') {
const { mockXHR } = require('../mock')
mockXHR()
}
// set ElementUI lang to EN
// Vue.use(ElementUI, { locale })
// 如果想要中文版 element-ui按如下方式声明
Vue.use(ElementUI)
import API from '@/api'
Vue.prototype.$API = API
// 全局组件 分页器
import Pagination from '@/components/Pagination/Pagination'
Vue.component('Pagination', Pagination)
// 封装一个message
Vue.prototype.message = function(type = 'warning', message) {
return this.$message({
message: message,
type
});
}
// 在 main.js 中注册全局过滤器
Vue.filter('formatDate', function(value) {
if (!value) return '';
// 解析日期
const date = new Date(value);
// 检查日期是否有效
if (isNaN(date)) return '';
// 格式化日期为 yyyy-MM-dd HH:mm:ss
return date.getFullYear() + '-' +
(date.getMonth() + 1).toString().padStart(2, '0') + '-' +
date.getDate().toString().padStart(2, '0') + ' ' +
date.getHours().toString().padStart(2, '0') + ':' +
date.getMinutes().toString().padStart(2, '0') + ':' +
date.getSeconds().toString().padStart(2, '0');
});
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
store,
beforeCreate() {
Vue.prototype.$bus = this
},
render: h => h(App)
})

63
src/permission.js Normal file
View File

@ -0,0 +1,63 @@
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
// 配置白名单
const whiteList = ['/login', '/register'] // no redirect whitelist
router.beforeEach(async(to, from, next) => {
// start progress bar
NProgress.start()
// set page title
document.title = getPageTitle(to.meta.title)
// determine whether the user has logged in
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' })
NProgress.done()
} else {
const hasGetUserInfo = store.getters.userId
if (hasGetUserInfo) {
next()
} else {
try {
// get user info
await store.dispatch('user/getInfo')
next()
} catch (error) {
// remove token and go to login page to re-login
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
/* has no token*/
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()
})

183
src/router/index.js Normal file
View File

@ -0,0 +1,183 @@
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'
/**
* Note: sub-menu only appear when route children.length >= 1
* Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
*
* hidden: true if set true, item will not show in the sidebar(default is false)
* alwaysShow: true if set true, will always show the root menu
* if not set alwaysShow, when item has more than one children route,
* it will becomes nested mode, otherwise not show the root menu
* redirect: noRedirect if set noRedirect will no redirect in the breadcrumb
* name:'router-name' the name is used by <keep-alive> (must set!!!)
* meta : {
roles: ['admin','editor'] control the page roles (you can set multiple roles)
title: 'title' the name show in sidebar and breadcrumb (recommend set)
icon: 'svg-name'/'el-icon-x' the icon show in the sidebar
breadcrumb: false if set false, the item will hidden in breadcrumb(default is true)
activeMenu: '/example/list' if set path, the sidebar will highlight the path you set
}
*/
/**
* constantRoutes
* a base page that does not have permission requirements
* all roles can be accessed
*/
export const constantRoutes = [{
// 用户登录路由
path: '/login',
component: () =>
import ('@/views/login/index.vue'),
hidden: true
},
{
// 用户注册路由
path: '/register',
name: 'register',
component: () =>
import ('@/views/register/index.vue'),
hidden: true
},
{
// 404路由
path: '/404',
component: () =>
import ('@/views/404'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/parking/parkingList',
meta: { title: '首页', icon: 'dashboard' },
children: [{
path: 'dashboard',
name: 'Dashboard',
component: () =>
import ('@/views/dashboard/index'),
meta: { title: '控制台', icon: 'dashboard' },
children: [{
path: 'cars',
name: 'Car',
component: () =>
import ('@/views/Car/Car.vue'),
meta: { title: '租车地图', icon: 'car' }
}]
}]
},
// 停车场
{
path: '/parking',
component: Layout,
redirect: '/parking/parkingList',
name: 'Parking',
meta: { title: '营业网店', icon: 'parking' },
children: [{
path: 'parkingList',
name: 'ParkingList',
component: () =>
import ('@/views/parking/parkingList'),
meta: { title: '网点列表' }
}]
},
{
path: '/forum',
component: Layout,
redirect: '/forum/forum',
name: 'forum',
meta: { title: '交流论坛', icon: 'pinpaizhuanqu' },
alwaysShow: true,
children: [
{
path: '/forum',
name: 'forum',
component: () => import('@/views/Forum/forum'),
meta: { title: '交流论坛' }
},
{
path: '/post/:id', // 动态参数 :id
name: 'post',
component: () => import('@/views/Forum/post'), // 加载帖子详情页面
meta: { title: '帖子详细' },
hidden: true
}
]
},
// 车辆管理
{
path: '/carManage',
component: Layout,
redirect: '/carManage/carList',
name: 'CarManage',
meta: { title: '订单管理', icon: 'cheliangguanli' },
alwaysShow: true,
children: [{
path: 'carList',
name: 'CarList',
component: () =>
import ('@/views/carManage/carList'),
meta: { title: '订单列表' }
}]
},
{
path: '/ai',
component: Layout,
redirect: '/ai/chat',
name: 'MemberManage',
meta: { title: '智能推荐', icon: 'el-icon-sort' },
alwaysShow: true,
children: [{
path: 'chat',
name: 'chat',
component: () =>
import ('@/views/Chat/index'),
meta: { title: '智能推荐' }
}]
},
// 会员管理
{
path: '/memberManage',
component: Layout,
redirect: '/memberManage/memberList',
name: 'MemberManage',
meta: { title: '个人中心', icon: 'member' },
alwaysShow: true,
children: [{
path: 'memberList',
name: 'MemberList',
component: () =>
import ('@/views/memberManage/memberList'),
meta: { title: '我的资料' }
}]
},
// 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }
]
const createRouter = () => new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
const router = createRouter()
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router

16
src/settings.js Normal file
View File

@ -0,0 +1,16 @@
module.exports = {
title: '基于Spring Boot的停车场管理系统-门户端',
/**
* @type {boolean} true | false
* @description Whether fix the header
*/
fixedHeader: false,
/**
* @type {boolean} true | false
* @description Whether show the logo in sidebar
*/
sidebarLogo: false
}

33
src/store/car.js Normal file
View File

@ -0,0 +1,33 @@
import { getCars } from '@/api/car'
import { Message } from 'element-ui'
const carsOptions = {
namespaced: true,
actions: {
// 获取停车场车辆列表数据
async getCarList(context, id) {
let result = await getCars(id)
if (result.status === 200) {
if (result.data.length === 0) {
Message({ type: 'warning', message: result.message })
}
context.commit('GETCARLIST', result.data)
}
}
},
mutations: {
GETCARLIST(state, data) {
state.carList = data
},
// 清空车辆列表
CLEARCARLIST(state) {
state.carList = []
}
},
state: {
carList: []
},
getters: {}
}
export default carsOptions

9
src/store/getters.js Normal file
View File

@ -0,0 +1,9 @@
const getters = {
sidebar: state => state.app.sidebar,
device: state => state.app.device,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
userId: state => window.localStorage.getItem('userId')
}
export default getters

22
src/store/index.js Normal file
View File

@ -0,0 +1,22 @@
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import app from './modules/app'
import settings from './modules/settings'
import user from './modules/user'
import parkingOptions from "./parking";
import carsOptions from "./car";
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
app,
settings,
user,
parkingAbout: parkingOptions,
carsAbout: carsOptions,
},
getters
})
export default store

50
src/store/modules/app.js Normal file
View File

@ -0,0 +1,50 @@
import Cookies from 'js-cookie'
const state = {
sidebar: {
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
withoutAnimation: false
},
device: 'desktop',
uploadURL: 'http://127.0.0.1:3007/car/upload/img'
// uploadURL: 'http://www.hualearn.site/car/upload/img'
}
const mutations = {
TOGGLE_SIDEBAR: state => {
state.sidebar.opened = !state.sidebar.opened
state.sidebar.withoutAnimation = false
if (state.sidebar.opened) {
Cookies.set('sidebarStatus', 1)
} else {
Cookies.set('sidebarStatus', 0)
}
},
CLOSE_SIDEBAR: (state, withoutAnimation) => {
Cookies.set('sidebarStatus', 0)
state.sidebar.opened = false
state.sidebar.withoutAnimation = withoutAnimation
},
TOGGLE_DEVICE: (state, device) => {
state.device = device
}
}
const actions = {
toggleSideBar({ commit }) {
commit('TOGGLE_SIDEBAR')
},
closeSideBar({ commit }, { withoutAnimation }) {
commit('CLOSE_SIDEBAR', withoutAnimation)
},
toggleDevice({ commit }, device) {
commit('TOGGLE_DEVICE', device)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@ -0,0 +1,32 @@
import defaultSettings from '@/settings'
const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
const state = {
showSettings: showSettings,
fixedHeader: fixedHeader,
sidebarLogo: sidebarLogo
}
const mutations = {
CHANGE_SETTING: (state, { key, value }) => {
// eslint-disable-next-line no-prototype-builtins
if (state.hasOwnProperty(key)) {
state[key] = value
}
}
}
const actions = {
changeSetting({ commit }, data) {
commit('CHANGE_SETTING', data)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

140
src/store/modules/user.js Normal file
View File

@ -0,0 +1,140 @@
import { login, logout, getInfo, sendcode, register } from '@/api/user'
import { getToken, setToken, removeToken, getUsername } from '@/utils/auth'
import { resetRouter } from '@/router'
const getDefaultState = () => {
return {
token: getToken(),
name: '',
avatar: '',
userId: ''
}
}
const state = getDefaultState()
const mutations = {
RESET_STATE: (state) => {
Object.assign(state, getDefaultState())
},
SET_TOKEN: (state, token) => {
state.token = token
},
SET_USERID: (state, userId) => {
state.userId = userId
},
SET_NAME: (state, userName) => {
state.name = userName
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
}
}
const actions = {
// 用户注册
async register({ commit }, loginForm) {
let data = new URLSearchParams()
data.append("Phone", loginForm.Phone.trim())
data.append("UserName", loginForm.UserName.trim())
data.append('PassWord', loginForm.PassWord.trim())
data.append('code', loginForm.verifycode.trim())
let result = await register(data);
if (result.status === 200) {
return result;
} else {
return Promise.reject(new Error('fail'))
}
},
async sendcode({ commit }, loginForm) {
let data = new URLSearchParams()
data.append("Phone", loginForm.Phone.trim())
let result = await sendcode(data);
if (result.status === 200) {
return result;
} else {
return Promise.reject(new Error('fail'))
}
},
// 用户登录
// user login
async login({ commit }, loginForm) {
let data = new URLSearchParams()
data.append("Phone", loginForm.Phone.trim())
data.append('PassWord', loginForm.PassWord.trim())
let result = await login(data);
if (result.status === 200) {
commit
commit('SET_TOKEN', result.token)
commit('SET_USERID', result.data)
setToken(result.token);
// 将userId存储到localstorage中
window.localStorage.setItem('userId', result.data)
return 'ok';
} else {
return Promise.reject(new Error('fail'))
}
},
//获取用户信息
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
let data = new URLSearchParams()
data.append("userId", localStorage.getItem("userId"))
getInfo(data).then(response => {
const { data } = response
if (!data) {
return reject('Verification failed, please Login again.')
}
const { userName, user_pic,avatar } = data[0]
console.log(userName)
console.log(user_pic)
console.log(data)
commit('SET_NAME', userName)
// 将userId存储到localstorage中
window.localStorage.setItem('userName', userName)
console.log(avatar)
commit('SET_AVATAR', avatar)
window.localStorage.setItem('avatar', avatar);
resolve(data)
}).catch(error => {
reject(error)
})
})
},
logout({ commit }) {
return new Promise((resolve) => {
// 清空localstorage
window.localStorage.clear();
// 移除token
removeToken();
// 重置路由
resetRouter();
// 清空vuex
commit('RESET_STATE');
// 等待异步操作完成后再跳转到登录页面
setTimeout(() => {
resolve();
next(`/login`);
}, 1000);
});
},
// remove token
resetToken({ commit }) {
return new Promise(resolve => {
removeToken() // must remove token first
commit('RESET_STATE')
resolve()
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

25
src/store/parking.js Normal file
View File

@ -0,0 +1,25 @@
import { getParking } from '@/api/parking'
const parkingOptions = {
namespaced: true,
actions: {
// 获取停车场数据
async getParking(context, value) {
let result = await getParking()
if (result.status === 200) {
context.commit('GETPARKING', result.data)
}
}
},
mutations: {
GETPARKING(state, parking) {
state.parkingList = parking
}
},
state: {
parkingList: []
},
getters: {}
}
export default parkingOptions

107
src/styles/element-ui.scss Normal file
View File

@ -0,0 +1,107 @@
// cover some element-ui styles
.el-breadcrumb__inner,
.el-breadcrumb__inner a {
font-weight: 400 !important;
}
.el-upload {
input[type="file"] {
display: none !important;
}
}
.el-upload__input {
display: none;
}
// to fixed https://github.com/ElemeFE/element/issues/2461
.el-dialog {
transform: none;
left: 0;
position: relative;
margin: 0 auto;
}
// refine element ui upload
.upload-container {
.el-upload {
width: 100%;
.el-upload-dragger {
width: 100%;
height: 200px;
}
}
}
// dropdown
.el-dropdown-menu {
a {
display: block
}
}
// to fix el-date-picker css style
.el-range-separator {
box-sizing: content-box;
}
ul, li { list-style: none; }
// 上传图片
.upload-image-wrap {
display: flex;
.upload-img {
position: relative;
background-color: #fbfdff;
border: 1px dashed #c0ccda;
border-radius: 6px;
box-sizing: border-box;
width: 148px;
height: 148px;
cursor: pointer;
line-height: 146px;
vertical-align: top;
text-align: center;
img {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
&:before {
content: "+";
font-size: 50px;
font-weight: 300;
color: #ccc;
}
}
.upload-img-list {
flex: 1;
padding-left: 30px;
li {
float: left;
width: 60px;
height: 60px;
margin: 0 10px;
img {
width: 100%;
height: 100%;
}
}
}
}
.el-form-item__content {
z-index: 200 ;
}
.el-dialog__body {
padding-left: 100px;
}
.text {
.el-row {
margin-top: 20px;
}
}

65
src/styles/index.scss Normal file
View File

@ -0,0 +1,65 @@
@import './variables.scss';
@import './mixin.scss';
@import './transition.scss';
@import './element-ui.scss';
@import './sidebar.scss';
body {
height: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}
label {
font-weight: 700;
}
html {
height: 100%;
box-sizing: border-box;
}
#app {
height: 100%;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
a:focus,
a:active {
outline: none;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
div:focus {
outline: none;
}
.clearfix {
&:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
}
// main-container global css
.app-container {
padding: 20px;
}

28
src/styles/mixin.scss Normal file
View File

@ -0,0 +1,28 @@
@mixin clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}
@mixin scrollBar {
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
@mixin relative {
position: relative;
width: 100%;
height: 100%;
}

226
src/styles/sidebar.scss Normal file
View File

@ -0,0 +1,226 @@
#app {
.main-container {
min-height: 100%;
transition: margin-left .28s;
margin-left: $sideBarWidth;
position: relative;
}
.sidebar-container {
transition: width 0.28s;
width: $sideBarWidth !important;
background-color: $menuBg;
height: 100%;
position: fixed;
font-size: 0px;
top: 0;
bottom: 0;
left: 0;
z-index: 1001;
overflow: hidden;
// reset element-ui css
.horizontal-collapse-transition {
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
}
.scrollbar-wrapper {
overflow-x: hidden !important;
}
.el-scrollbar__bar.is-vertical {
right: 0px;
}
.el-scrollbar {
height: 100%;
}
&.has-logo {
.el-scrollbar {
height: calc(100% - 50px);
}
}
.is-horizontal {
display: none;
}
a {
display: inline-block;
width: 100%;
overflow: hidden;
}
.svg-icon {
margin-right: 16px;
}
.sub-el-icon {
margin-right: 12px;
margin-left: -2px;
}
.el-menu {
border: none;
height: 100%;
width: 100% !important;
}
// menu hover
.submenu-title-noDropdown,
.el-submenu__title {
&:hover {
background-color: $menuHover !important;
}
}
.is-active>.el-submenu__title {
color: $subMenuActiveText !important;
}
& .nest-menu .el-submenu>.el-submenu__title,
& .el-submenu .el-menu-item {
min-width: $sideBarWidth !important;
background-color: $subMenuBg !important;
&:hover {
background-color: $subMenuHover !important;
}
}
}
.hideSidebar {
.sidebar-container {
width: 54px !important;
}
.main-container {
margin-left: 54px;
}
.submenu-title-noDropdown {
padding: 0 !important;
position: relative;
.el-tooltip {
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
.sub-el-icon {
margin-left: 19px;
}
}
}
.el-submenu {
overflow: hidden;
&>.el-submenu__title {
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
.sub-el-icon {
margin-left: 19px;
}
.el-submenu__icon-arrow {
display: none;
}
}
}
.el-menu--collapse {
.el-submenu {
&>.el-submenu__title {
&>span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
}
}
}
}
.el-menu--collapse .el-menu .el-submenu {
min-width: $sideBarWidth !important;
}
// mobile responsive
.mobile {
.main-container {
margin-left: 0px;
}
.sidebar-container {
transition: transform .28s;
width: $sideBarWidth !important;
}
&.hideSidebar {
.sidebar-container {
pointer-events: none;
transition-duration: 0.3s;
transform: translate3d(-$sideBarWidth, 0, 0);
}
}
}
.withoutAnimation {
.main-container,
.sidebar-container {
transition: none;
}
}
}
// when menu collapsed
.el-menu--vertical {
&>.el-menu {
.svg-icon {
margin-right: 16px;
}
.sub-el-icon {
margin-right: 12px;
margin-left: -2px;
}
}
.nest-menu .el-submenu>.el-submenu__title,
.el-menu-item {
&:hover {
// you can use $subMenuHover
background-color: $menuHover !important;
}
}
// the scroll bar appears when the subMenu is too long
>.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;
}
}
}

View File

@ -0,0 +1,48 @@
// global transition css
/* fade */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.28s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all .5s;
}
.fade-transform-enter {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* breadcrumb transition */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all .5s;
}
.breadcrumb-enter,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}
.breadcrumb-move {
transition: all .5s;
}
.breadcrumb-leave-active {
position: absolute;
}

25
src/styles/variables.scss Normal file
View File

@ -0,0 +1,25 @@
// sidebar
$menuText:#bfcbd9;
$menuActiveText:#409EFF;
$subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
$menuBg:#304156;
$menuHover:#263445;
$subMenuBg:#1f2d3d;
$subMenuHover:#001528;
$sideBarWidth: 210px;
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
menuText: $menuText;
menuActiveText: $menuActiveText;
subMenuActiveText: $subMenuActiveText;
menuBg: $menuBg;
menuHover: $menuHover;
subMenuBg: $subMenuBg;
subMenuHover: $subMenuHover;
sideBarWidth: $sideBarWidth;
}

23
src/utils/auth.js Normal file
View File

@ -0,0 +1,23 @@
import Cookies from 'js-cookie'
const TokenKey = 'vue_admin_template_token'
const userNameKey = "username"
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
export function getUsername() {
return Cookies.get(userNameKey);
}
export function setUsername(value){
return Cookies.set(userNameKey, value);
}

View File

@ -0,0 +1,10 @@
import defaultSettings from '@/settings'
const title = defaultSettings.title || 'Vue Admin Template'
export default function getPageTitle(pageTitle) {
if (pageTitle) {
return `${pageTitle} - ${title}`
}
return `${title}`
}

118
src/utils/index.js Normal file
View File

@ -0,0 +1,118 @@
/**
* Created by PanJiaChen on 16/11/18.
*/
/**
* Parse the time to string
* @param {(Object|string|number)} time
* @param {string} cFormat
* @returns {string | null}
*/
// decodeURI
export function parseTime(time, cFormat) {
if (arguments.length === 0 || !time) {
return null
}
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'object') {
date = time
} else {
if ((typeof time === 'string')) {
if ((/^[0-9]+$/.test(time))) {
// support "1548221490638"
time = parseInt(time)
} else {
// support safari
// https://stackoverflow.com/questions/4310953/invalid-date-in-safari
time = time.replace(new RegExp(/-/gm), '/')
}
}
if ((typeof time === 'number') && (time.toString().length === 10)) {
time = time * 1000
}
date = new Date(time)
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
}
const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
const value = formatObj[key]
// Note: getDay() returns 0 on Sunday
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
return value.toString().padStart(2, '0')
})
return time_str
}
/**
* @param {number} time
* @param {string} option
* @returns {string}
*/
export function formatTime(time, option) {
if (('' + time).length === 10) {
time = parseInt(time) * 1000
} else {
time = +time
}
const d = new Date(time)
const now = Date.now()
const diff = (now - d) / 1000
if (diff < 30) {
return '刚刚'
} else if (diff < 3600) {
// less 1 hour
return Math.ceil(diff / 60) + '分钟前'
} else if (diff < 3600 * 24) {
return Math.ceil(diff / 3600) + '小时前'
} else if (diff < 3600 * 24 * 2) {
return '1天前'
}
if (option) {
return parseTime(time, option)
} else {
return (
d.getMonth() +
1 +
'月' +
d.getDate() +
'日' +
d.getHours() +
'时' +
d.getMinutes() +
'分'
)
}
}
/**
* @param {string} url
* @returns {Object}
*/
export 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
}

147
src/utils/request.js Normal file
View File

@ -0,0 +1,147 @@
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
import router from "@/router"; //看具体项目封装的路由文件修改
// create an axios instance
const service = axios.create({
// baseURL: process.env.VUE_APP_BASE_API,
baseURL: 'http://127.0.0.1:8898/api',
timeout: 5000
})
//跳转登陆页面
const toLogin = () => {
// 执行退出操作
store.dispatch('user/logout').then(() => {
// 返回登录页面
router.push('/login')
})
};
//封装状态码错误处理函数
const errorHandle = status => {
switch (status) {
//登录不成功时跳转到登录页面
case 401:
console.log("认证失败,未登录或无权限");
toLogin();
break;
case 403:
//token过期了,清除token存储
localStorage.removeItem("token");
console.log("token校验失败");
toLogin();
break;
case 404:
console.log("请求的资源不存在");
break;
default:
console.log("请求出错,状态码为:" + status);
break;
}
};
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
if (store.getters.token) {
// let each request carry token
// ['X-Token'] is a custom headers key
// please modify it according to the actual situation
config.headers['Authorization'] = getToken(); // 携带token
config.headers['Content-Type'] = 'application/x-www-form-urlencoded'
// config.headers['Username'] = getUsername(); // 携带token
return config;
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
response => {
const res = response.data
console.log(res.status); // 添加此行
if (res.status == 401) {
toLogin()
}
// if the custom code is not 20000, it is judged as an error.
if (res.status !== 20000 && res.status !== 200 && res.status !== 201) {
Message({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000
})
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (res.status === 50008 || res.status === 50012 || res.status === 50014) {
// to re-login
MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
confirmButtonText: 'Re-Login',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res
}
// const data = response.data;
// // console.log(data);
// // 不为0即接口异常时
// if (data.resCode !== 0) {
// Message.error(data.message);
// return Promise.reject(data);
// } else {
// return data; // return Promise.resolve(data);
// }
},
error => {
// let { response } = error;
//判断是否为重复请求而出错
// if (isCancel(error)) console.log("请求失败,原因是" + error.message);
if (error.response) {
errorHandle(error.response.status);
}
//判断客户端有无联网
if (!window.navigator.onLine) {
//断网处理:跳转断网页面/提示网络错误等等
alert("请检查网络是否连接");
}
//出错中断promise链
// return new Promise(() => {});
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service

20
src/utils/validate.js Normal file
View File

@ -0,0 +1,20 @@
/**
* Created by PanJiaChen on 16/11/18.
*/
/**
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUsername(str) {
const valid_map = ['admin', 'editor']
return valid_map.indexOf(str.trim()) >= 0
}

Some files were not shown because too many files have changed in this diff Show More