first commit
This commit is contained in:
commit
8d0158be7c
5
.editorconfig
Normal file
5
.editorconfig
Normal file
@ -0,0 +1,5 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
18
.env.development
Normal file
18
.env.development
Normal file
@ -0,0 +1,18 @@
|
||||
NODE_ENV = 'development'
|
||||
|
||||
ENV = 'development'
|
||||
|
||||
VITE_PUBLIC_PATH = '/'
|
||||
|
||||
# 是否使用MOCK
|
||||
VITE_APP_USE_MOCK = true
|
||||
|
||||
# proxy
|
||||
VITE_PROXY = [["/api","http://localhost:8080"],["/api-test","localhost:8080"]]
|
||||
|
||||
# base api
|
||||
# VITE_APP_GLOB_BASE_API = '/api'
|
||||
VITE_APP_GLOB_BASE_API = '/api-mock'
|
||||
|
||||
# test base api
|
||||
VITE_APP_GLOB_BASE_API_TEST = '/api-test'
|
14
.env.production
Normal file
14
.env.production
Normal file
@ -0,0 +1,14 @@
|
||||
NODE_ENV = 'production'
|
||||
|
||||
ENV = 'production'
|
||||
|
||||
VITE_PUBLIC_PATH = '/'
|
||||
|
||||
# 是否使用MOCK
|
||||
VITE_APP_USE_MOCK = true
|
||||
|
||||
# base api
|
||||
VITE_APP_GLOB_BASE_API = '/api-mock'
|
||||
|
||||
# test base api
|
||||
VITE_APP_GLOB_BASE_API_TEST = '/api-test'
|
14
.env.staging
Normal file
14
.env.staging
Normal file
@ -0,0 +1,14 @@
|
||||
NODE_ENV = 'production'
|
||||
|
||||
ENV = 'staging'
|
||||
|
||||
VITE_PUBLIC_PATH = '/'
|
||||
|
||||
# 是否使用MOCK
|
||||
VITE_APP_USE_MOCK = false
|
||||
|
||||
# base api
|
||||
VITE_APP_GLOB_BASE_API = '/api-mock'
|
||||
|
||||
# test base api
|
||||
VITE_APP_GLOB_BASE_API_TEST = '/api-test'
|
14
.env.test
Normal file
14
.env.test
Normal file
@ -0,0 +1,14 @@
|
||||
NODE_ENV = 'production'
|
||||
|
||||
ENV = 'test'
|
||||
|
||||
VITE_PUBLIC_PATH = '/'
|
||||
|
||||
# 是否使用MOCK
|
||||
VITE_APP_USE_MOCK = false
|
||||
|
||||
# base api
|
||||
VITE_APP_GLOB_BASE_API = '/api-mock'
|
||||
|
||||
# test base api
|
||||
VITE_APP_GLOB_BASE_API_TEST = '/api-test'
|
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
public
|
26
.eslintrc.js
Normal file
26
.eslintrc.js
Normal file
@ -0,0 +1,26 @@
|
||||
// * https://zhuanlan.zhihu.com/p/388703150
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true, // browser global variables
|
||||
node: true,
|
||||
es2021: true, // adds all ECMAScript 2021 globals and automatically sets the ecmaVersion parser option to 12.
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
},
|
||||
parser: 'vue-eslint-parser',
|
||||
extends: ['plugin:vue/vue3-recommended', 'plugin:prettier/recommended'],
|
||||
plugins: ['prettier'],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
'vue/valid-template-root': 'off',
|
||||
'vue/no-multiple-template-root': 'off',
|
||||
'vue/multi-word-component-names': [
|
||||
'error',
|
||||
{
|
||||
ignores: ['index', '401', '404'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
*.local
|
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
||||
/node_modules/**
|
||||
/dist/*
|
||||
/public/*
|
26
.vscode/settings.json
vendored
Normal file
26
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"files.eol": "\n",
|
||||
"path-intellisense.mappings": {
|
||||
"@/": "${workspaceRoot}/src"
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "vscode.html-language-features"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"eslint.validate": ["javascript", "javascriptreact", "typescript"]
|
||||
}
|
3
build/constant.js
Normal file
3
build/constant.js
Normal file
@ -0,0 +1,3 @@
|
||||
export const GLOB_CONFIG_FILE_NAME = 'app.config.js'
|
||||
export const GLOB_CONFIG_NAME = '__APP__GLOB__CONF__'
|
||||
export const OUTPUT_DIR = 'dist'
|
29
build/script/build-config.js
Normal file
29
build/script/build-config.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { GLOB_CONFIG_FILE_NAME, GLOB_CONFIG_NAME, OUTPUT_DIR } from '../constant'
|
||||
import fs, { writeFileSync } from 'fs-extra'
|
||||
import chalk from 'chalk'
|
||||
import { getEnvConfig, getRootPath } from '../utils'
|
||||
|
||||
function createConfig(option) {
|
||||
const { config, configName, configFileName } = option
|
||||
try {
|
||||
const windowConf = `window.${configName}`
|
||||
const configStr = `${windowConf}=${JSON.stringify(config)};
|
||||
Object.freeze(${windowConf});
|
||||
Object.defineProperty(window, "${configName}", {
|
||||
configurable: false,
|
||||
writable: false,
|
||||
});
|
||||
`.replace(/\s/g, '')
|
||||
fs.mkdirp(getRootPath(OUTPUT_DIR))
|
||||
writeFileSync(getRootPath(`${OUTPUT_DIR}/${configFileName}`), configStr)
|
||||
} catch (error) {
|
||||
console.log(chalk.red('configuration file configuration file failed to package:\n' + error))
|
||||
}
|
||||
}
|
||||
|
||||
export function runBuildConfig() {
|
||||
const config = getEnvConfig()
|
||||
const configName = GLOB_CONFIG_NAME
|
||||
const configFileName = GLOB_CONFIG_FILE_NAME
|
||||
createConfig({ config, configName, configFileName })
|
||||
}
|
14
build/script/index.js
Normal file
14
build/script/index.js
Normal file
@ -0,0 +1,14 @@
|
||||
import chalk from 'chalk'
|
||||
import { runBuildConfig } from './build-config'
|
||||
|
||||
export const runBuild = async () => {
|
||||
try {
|
||||
runBuildConfig()
|
||||
console.log(`✨ ${chalk.cyan('build successfully!')}`)
|
||||
} catch (error) {
|
||||
console.log(chalk.red('vite build error:\n' + error))
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
runBuild()
|
69
build/utils.js
Normal file
69
build/utils.js
Normal file
@ -0,0 +1,69 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
export function wrapperEnv(envOptions) {
|
||||
if (!envOptions) return {}
|
||||
const ret = {}
|
||||
|
||||
for (const key in envOptions) {
|
||||
let val = envOptions[key]
|
||||
if (['true', 'false'].includes(val)) {
|
||||
val = val === 'true'
|
||||
}
|
||||
if (['VITE_PORT'].includes(key)) {
|
||||
val = +val
|
||||
}
|
||||
if (key === 'VITE_PROXY' && val) {
|
||||
try {
|
||||
val = JSON.parse(val.replace(/'/g, '"'))
|
||||
} catch (error) {
|
||||
val = ''
|
||||
}
|
||||
}
|
||||
ret[key] = val
|
||||
if (typeof key === 'string') {
|
||||
process.env[key] = val
|
||||
} else if (typeof key === 'object') {
|
||||
process.env[key] = JSON.stringify(val)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前环境下生效的配置文件名
|
||||
*/
|
||||
function getConfFiles() {
|
||||
const script = process.env.npm_lifecycle_script
|
||||
const reg = new RegExp('--mode ([a-z_\\d]+)')
|
||||
const result = reg.exec(script)
|
||||
if (result) {
|
||||
const mode = result[1]
|
||||
return ['.env', `.env.${mode}`]
|
||||
}
|
||||
return ['.env', '.env.production']
|
||||
}
|
||||
|
||||
export function getEnvConfig(match = 'VITE_APP_GLOB_', confFiles = getConfFiles()) {
|
||||
let envConfig = {}
|
||||
confFiles.forEach((item) => {
|
||||
try {
|
||||
const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item)))
|
||||
envConfig = { ...envConfig, ...env }
|
||||
} catch (e) {
|
||||
console.error(`Error in parsing ${item}`, e)
|
||||
}
|
||||
})
|
||||
const reg = new RegExp(`^(${match})`)
|
||||
Object.keys(envConfig).forEach((key) => {
|
||||
if (!reg.test(key)) {
|
||||
Reflect.deleteProperty(envConfig, key)
|
||||
}
|
||||
})
|
||||
return envConfig
|
||||
}
|
||||
|
||||
export function getRootPath(...dir) {
|
||||
return path.resolve(process.cwd(), ...dir)
|
||||
}
|
32
build/vite/plugin/html.js
Normal file
32
build/vite/plugin/html.js
Normal file
@ -0,0 +1,32 @@
|
||||
import html from 'vite-plugin-html'
|
||||
import { version } from '../../../package.json'
|
||||
import { GLOB_CONFIG_FILE_NAME } from '../../constant'
|
||||
|
||||
export function configHtmlPlugin(viteEnv, isBuild) {
|
||||
const { VITE_APP_TITLE, VITE_PUBLIC_PATH } = viteEnv
|
||||
const path = VITE_PUBLIC_PATH.endsWith('/') ? VITE_PUBLIC_PATH : `${VITE_PUBLIC_PATH}/`
|
||||
|
||||
const getAppConfigSrc = () => {
|
||||
return `${path}${GLOB_CONFIG_FILE_NAME}?v=${version}-${new Date().getTime()}`
|
||||
}
|
||||
|
||||
const htmlPlugin = html({
|
||||
minify: isBuild,
|
||||
inject: {
|
||||
data: {
|
||||
title: VITE_APP_TITLE,
|
||||
},
|
||||
tags: isBuild
|
||||
? [
|
||||
{
|
||||
tag: 'script',
|
||||
attrs: {
|
||||
src: getAppConfigSrc(),
|
||||
},
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
})
|
||||
return htmlPlugin
|
||||
}
|
12
build/vite/plugin/index.js
Normal file
12
build/vite/plugin/index.js
Normal file
@ -0,0 +1,12 @@
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { configHtmlPlugin } from './html'
|
||||
import { configMockPlugin } from './mock'
|
||||
import { unocss } from './unocss'
|
||||
|
||||
export function createVitePlugins(viteEnv, isBuild) {
|
||||
const plugins = [vue(), unocss(), configHtmlPlugin(viteEnv, isBuild)]
|
||||
|
||||
viteEnv?.VITE_APP_USE_MOCK && plugins.push(configMockPlugin(isBuild))
|
||||
|
||||
return plugins
|
||||
}
|
14
build/vite/plugin/mock.js
Normal file
14
build/vite/plugin/mock.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { viteMockServe } from 'vite-plugin-mock'
|
||||
|
||||
export function configMockPlugin(isBuild) {
|
||||
return viteMockServe({
|
||||
ignore: /^\_/,
|
||||
mockPath: 'mock',
|
||||
localEnabled: !isBuild,
|
||||
prodEnabled: isBuild,
|
||||
injectCode: `
|
||||
import { setupProdMockServer } from '../mock/_createProdServer';
|
||||
setupProdMockServer();
|
||||
`,
|
||||
})
|
||||
}
|
9
build/vite/plugin/unocss.js
Normal file
9
build/vite/plugin/unocss.js
Normal file
@ -0,0 +1,9 @@
|
||||
import Unocss from 'unocss/vite'
|
||||
import { presetUno, presetAttributify, presetIcons } from 'unocss'
|
||||
|
||||
// https://github.com/antfu/unocss
|
||||
export function unocss() {
|
||||
return Unocss({
|
||||
presets: [presetUno(), presetAttributify(), presetIcons()],
|
||||
})
|
||||
}
|
18
build/vite/proxy.js
Normal file
18
build/vite/proxy.js
Normal file
@ -0,0 +1,18 @@
|
||||
const httpsRE = /^https:\/\//
|
||||
export function createProxy(list = []) {
|
||||
const ret = {}
|
||||
for (const [prefix, target] of list) {
|
||||
const isHttps = httpsRE.test(target)
|
||||
|
||||
// https://github.com/http-party/node-http-proxy#options
|
||||
ret[prefix] = {
|
||||
target: target,
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''),
|
||||
// https is require secure=false
|
||||
...(isHttps ? { secure: false } : {}),
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
22
index.html
Normal file
22
index.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="cn">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Cache-control" content="no-cache" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<title>
|
||||
<%= title %>
|
||||
</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
9
jsconfig.json
Normal file
9
jsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
14
mock/_createProdServer.js
Normal file
14
mock/_createProdServer.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
|
||||
|
||||
const modules = import.meta.globEager('./**/*.js')
|
||||
const mockModules = []
|
||||
Object.keys(modules).forEach((key) => {
|
||||
if (key.includes('/_')) {
|
||||
return
|
||||
}
|
||||
mockModules.push(...modules[key].default)
|
||||
})
|
||||
|
||||
export function setupProdMockServer() {
|
||||
createProdMockServer(mockModules)
|
||||
}
|
12
mock/_utils.js
Normal file
12
mock/_utils.js
Normal file
@ -0,0 +1,12 @@
|
||||
export function resolveToken(authorization) {
|
||||
/**
|
||||
* * jwt token
|
||||
* * Bearer + token
|
||||
* ! 认证方案: Bearer
|
||||
*/
|
||||
const reqTokenSplit = authorization.split(' ')
|
||||
if (reqTokenSplit.length === 2) {
|
||||
return reqTokenSplit[1]
|
||||
}
|
||||
return ''
|
||||
}
|
40
mock/auth/index.js
Normal file
40
mock/auth/index.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { resolveToken } from '../_utils'
|
||||
|
||||
const token = {
|
||||
admin: 'admin',
|
||||
editor: 'editor',
|
||||
}
|
||||
|
||||
export default [
|
||||
{
|
||||
url: '/api-mock/auth/login',
|
||||
method: 'post',
|
||||
response: ({ body }) => {
|
||||
if (['admin', 'editor'].includes(body?.name)) {
|
||||
return {
|
||||
code: 0,
|
||||
data: {
|
||||
token: token[body.name],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
code: -1,
|
||||
message: '没有此用户',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
url: '/api-mock/auth/refreshToken',
|
||||
method: 'post',
|
||||
response: ({ headers }) => {
|
||||
return {
|
||||
code: 0,
|
||||
data: {
|
||||
token: resolveToken(headers?.authorization),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
39
mock/user/index.js
Normal file
39
mock/user/index.js
Normal file
@ -0,0 +1,39 @@
|
||||
import { resolveToken } from '../_utils'
|
||||
|
||||
const users = {
|
||||
admin: {
|
||||
id: 1,
|
||||
name: '大脸怪(admin)',
|
||||
avatar: 'https://gitee.com/zclzone/res/raw/master/qs-zone/blob/img/lADPDiQ3QDTwsz3NAarNAaw_428_426.jpg',
|
||||
email: 'Ronnie@123.com',
|
||||
role: ['admin'],
|
||||
},
|
||||
editor: {
|
||||
id: 2,
|
||||
name: '大脸怪(editor)',
|
||||
avatar: 'https://gitee.com/zclzone/res/raw/master/qs-zone/blob/img/lADPDiQ3QDTwsz3NAarNAaw_428_426.jpg',
|
||||
email: 'Ronnie@123.com',
|
||||
role: ['editor'],
|
||||
},
|
||||
guest: {
|
||||
id: 3,
|
||||
name: '访客(guest)',
|
||||
avatar: 'https://gitee.com/zclzone/res/raw/master/qs-zone/blob/img/lADPDiQ3QDTwsz3NAarNAaw_428_426.jpg',
|
||||
role: [],
|
||||
},
|
||||
}
|
||||
export default [
|
||||
{
|
||||
url: '/api-mock/user',
|
||||
method: 'get',
|
||||
response: ({ headers }) => {
|
||||
const token = resolveToken(headers?.authorization)
|
||||
return {
|
||||
code: 0,
|
||||
data: {
|
||||
...(users[token] || users.guest),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
2945
package-lock.json
generated
Normal file
2945
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
package.json
Normal file
46
package.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "vue-naive-admin",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"lint": "eslint . --fix",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"build": "vite build && esno ./build/script",
|
||||
"build:test": "vite build --mode test && esno ./build/script",
|
||||
"build:staging": "vite build --mode staging && esno ./build/script",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vicons/fa": "^0.11.0",
|
||||
"axios": "^0.21.4",
|
||||
"dayjs": "^1.10.7",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mockjs": "^1.1.0",
|
||||
"pinia": "^2.0.9",
|
||||
"vue": "^3.2.6",
|
||||
"vue-router": "^4.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@unocss/preset-attributify": "^0.16.3",
|
||||
"@unocss/preset-icons": "^0.16.3",
|
||||
"@unocss/preset-uno": "^0.16.3",
|
||||
"@vitejs/plugin-vue": "^1.6.0",
|
||||
"@vue/compiler-sfc": "^3.0.5",
|
||||
"chalk": "^5.0.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"eslint": "^8.6.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-vue": "^8.2.0",
|
||||
"esno": "^0.13.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"naive-ui": "^2.19.1",
|
||||
"prettier": "^2.5.1",
|
||||
"sass": "^1.38.1",
|
||||
"unocss": "^0.16.3",
|
||||
"vite": "^2.7.6",
|
||||
"vite-plugin-html": "^2.1.1",
|
||||
"vite-plugin-mock": "^2.9.6"
|
||||
}
|
||||
}
|
6
prettier.config.js
Normal file
6
prettier.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
printWidth: 120,
|
||||
singleQuote: true,
|
||||
semi: false,
|
||||
endOfLine: 'lf',
|
||||
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
25
src/App.vue
Normal file
25
src/App.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<script setup>
|
||||
import AppProvider from '@/components/AppProvider/index.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view>
|
||||
<template #default="{ Component, route }">
|
||||
<app-provider>
|
||||
<keep-alive v-if="route.meta && route.meta.keepAlive">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
<component :is="Component" v-else :key="route.fullPath" />
|
||||
</app-provider>
|
||||
</template>
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#app {
|
||||
height: 100%;
|
||||
.n-config-provider {
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
16
src/api/auth/index.js
Normal file
16
src/api/auth/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { defAxios } from '@/utils/http'
|
||||
|
||||
export const login = (data) => {
|
||||
return defAxios({
|
||||
url: '/auth/login',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const refreshToken = () => {
|
||||
return defAxios({
|
||||
url: '/auth/refreshToken',
|
||||
method: 'post',
|
||||
})
|
||||
}
|
38
src/api/user/index.js
Normal file
38
src/api/user/index.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { defAxios, mockAxios } from '@/utils/http'
|
||||
|
||||
export function getUsers(data = {}) {
|
||||
return defAxios({
|
||||
url: '/users',
|
||||
method: 'get',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function getUser(id) {
|
||||
if (id) {
|
||||
return defAxios({
|
||||
url: `/user/${id}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
return defAxios({
|
||||
url: '/user',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function saveUser(data = {}, id) {
|
||||
if (id) {
|
||||
return defAxios({
|
||||
url: '/user',
|
||||
method: 'put',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
return defAxios({
|
||||
url: `/user/${id}`,
|
||||
method: 'put',
|
||||
data,
|
||||
})
|
||||
}
|
52
src/components/AppProvider/DialogContent.vue
Normal file
52
src/components/AppProvider/DialogContent.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<script setup>
|
||||
import { isNullOrUndef } from '@/utils/is'
|
||||
import { useDialog } from 'naive-ui'
|
||||
|
||||
const NDialog = useDialog()
|
||||
|
||||
class Dialog {
|
||||
success(title, option) {
|
||||
this.showDialog('success', { title, ...option })
|
||||
}
|
||||
|
||||
warning(title, option) {
|
||||
this.showDialog('warning', { title, ...option })
|
||||
}
|
||||
|
||||
error(title, option) {
|
||||
this.showDialog('error', { title, ...option })
|
||||
}
|
||||
|
||||
showDialog(type = 'success', option) {
|
||||
if (isNullOrUndef(option.title)) {
|
||||
// ! 没有title的情况
|
||||
option.showIcon = false
|
||||
}
|
||||
NDialog[type]({
|
||||
positiveText: 'OK',
|
||||
closable: false,
|
||||
...option,
|
||||
})
|
||||
}
|
||||
|
||||
confirm(option = {}) {
|
||||
this.showDialog(option.type || 'error', {
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: option.confirm,
|
||||
onNegativeClick: option.cancel,
|
||||
onMaskClick: option.cancel,
|
||||
...option,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
window['$dialog'] = new Dialog()
|
||||
Object.freeze(window.$dialog)
|
||||
Object.defineProperty(window, '$dialog', {
|
||||
configurable: false,
|
||||
writable: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template></template>
|
10
src/components/AppProvider/LoadingBar.vue
Normal file
10
src/components/AppProvider/LoadingBar.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<script setup>
|
||||
import { useLoadingBar } from 'naive-ui'
|
||||
window['$loadingBar'] = useLoadingBar()
|
||||
Object.defineProperty(window, '$loadingBar', {
|
||||
configurable: false,
|
||||
writable: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template></template>
|
73
src/components/AppProvider/MessageContent.vue
Normal file
73
src/components/AppProvider/MessageContent.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<script setup>
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
const NMessage = useMessage()
|
||||
|
||||
let loadingMessage = null
|
||||
|
||||
class Message {
|
||||
/**
|
||||
* 规则:
|
||||
* * 同一Message实例只显示一个loading message,如果需要显示多个可以创建多个Message实例
|
||||
* * 新的message会替换正在显示的loading message
|
||||
* * 默认已创建一个Message实例$message挂载到window,同时也将Message类挂载到了window
|
||||
*/
|
||||
|
||||
removeMessage(message, duration = 2000) {
|
||||
setTimeout(() => {
|
||||
if (message) {
|
||||
message.destroy()
|
||||
message = null
|
||||
}
|
||||
}, duration)
|
||||
}
|
||||
|
||||
showMessage(type, content, option = {}) {
|
||||
if (this.loadingMessage && this.loadingMessage.type === 'loading') {
|
||||
// 如果存在则替换正在显示的loading message
|
||||
this.loadingMessage.type = type
|
||||
this.loadingMessage.content = content
|
||||
|
||||
if (type !== 'loading') {
|
||||
// 非loading message需设置自动清除
|
||||
this.removeMessage(this.loadingMessage, option.duration)
|
||||
}
|
||||
} else {
|
||||
// 不存在正在显示的loading则新建一个message,如果新建的message是loading message则将message赋值存储下来
|
||||
let message = NMessage[type](content, option)
|
||||
if (type === 'loading') {
|
||||
this.loadingMessage = message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loading(content) {
|
||||
this.showMessage('loading', content, { duration: 0 })
|
||||
}
|
||||
|
||||
success(content, option = {}) {
|
||||
this.showMessage('success', content, option)
|
||||
}
|
||||
|
||||
error(content, option = {}) {
|
||||
this.showMessage('error', content, option)
|
||||
}
|
||||
|
||||
info(content, option = {}) {
|
||||
this.showMessage('info', content, option)
|
||||
}
|
||||
|
||||
warning(content, option = {}) {
|
||||
this.showMessage('warning', content, option)
|
||||
}
|
||||
}
|
||||
|
||||
window['$message'] = new Message()
|
||||
|
||||
Object.defineProperty(window, '$message', {
|
||||
configurable: false,
|
||||
writable: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template></template>
|
26
src/components/AppProvider/index.vue
Normal file
26
src/components/AppProvider/index.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
import { NConfigProvider, NGlobalStyle, NLoadingBarProvider, NMessageProvider, NDialogProvider } from 'naive-ui'
|
||||
|
||||
import MessageContent from './MessageContent.vue'
|
||||
import DialogContent from './DialogContent.vue'
|
||||
import LoadingBar from './LoadingBar.vue'
|
||||
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider :theme-overrides="appStore.themeOverrides">
|
||||
<n-global-style />
|
||||
<n-loading-bar-provider>
|
||||
<loading-bar />
|
||||
<n-dialog-provider>
|
||||
<dialog-content />
|
||||
<n-message-provider>
|
||||
<message-content />
|
||||
<slot name="default"></slot>
|
||||
</n-message-provider>
|
||||
</n-dialog-provider>
|
||||
</n-loading-bar-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
9
src/layout/components/AppMain.vue
Normal file
9
src/layout/components/AppMain.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<router-view>
|
||||
<template #default="{ Component, route }">
|
||||
<transition name="fade-slide" mode="out-in" appear>
|
||||
<component :is="Component" :key="route.fullPath"></component>
|
||||
</transition>
|
||||
</template>
|
||||
</router-view>
|
||||
</template>
|
19
src/layout/components/header/BreadCrumb.vue
Normal file
19
src/layout/components/header/BreadCrumb.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<script setup>
|
||||
import { NBreadcrumb, NBreadcrumbItem } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
const { currentRoute } = router
|
||||
|
||||
function handleBreadClick(path) {
|
||||
if (path === currentRoute.value.path) return
|
||||
router.push(path)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-breadcrumb>
|
||||
<n-breadcrumb-item v-for="item in currentRoute.matched" :key="item.path" @click="handleBreadClick(item.path)">
|
||||
{{ item.meta.title }}
|
||||
</n-breadcrumb-item>
|
||||
</n-breadcrumb>
|
||||
</template>
|
47
src/layout/components/header/HeaderAction.vue
Normal file
47
src/layout/components/header/HeaderAction.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<script setup>
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { NDropdown } from 'naive-ui'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: '退出登录',
|
||||
key: 'logout',
|
||||
},
|
||||
]
|
||||
|
||||
function handleSelect(key) {
|
||||
if (key === 'logout') {
|
||||
userStore.logout()
|
||||
$message.success('已退出登录')
|
||||
router.push({ path: '/login' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-dropdown :options="options" @select="handleSelect">
|
||||
<div class="avatar">
|
||||
<img :src="userStore.avatar" />
|
||||
<span>{{ userStore.name }}</span>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
img {
|
||||
width: 100%;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
21
src/layout/components/header/index.vue
Normal file
21
src/layout/components/header/index.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import BreadCrumb from './BreadCrumb.vue'
|
||||
import HeaderAction from './HeaderAction.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="header">
|
||||
<bread-crumb />
|
||||
<header-action />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
padding: 0 35px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
32
src/layout/components/sidebar/SideLogo.vue
Normal file
32
src/layout/components/sidebar/SideLogo.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<script setup>
|
||||
import { NGradientText, NIcon } from 'naive-ui'
|
||||
import { LastfmSquare } from '@vicons/fa'
|
||||
const title = import.meta.env.VITE_APP_TITLE
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="logo">
|
||||
<n-icon size="36" color="#316c72">
|
||||
<lastfm-square />
|
||||
</n-icon>
|
||||
<router-link to="/">
|
||||
<n-gradient-text type="primary">{{ title }}</n-gradient-text>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.logo {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
a {
|
||||
margin-left: 5px;
|
||||
.n-gradient-text {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
160
src/layout/components/sidebar/SideMenu.vue
Normal file
160
src/layout/components/sidebar/SideMenu.vue
Normal file
@ -0,0 +1,160 @@
|
||||
<script setup>
|
||||
import { NMenu } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
|
||||
const router = useRouter()
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const { currentRoute } = router
|
||||
const routes = permissionStore.routes
|
||||
|
||||
const menuOptions = computed(() => {
|
||||
return generateOptions(routes, '')
|
||||
})
|
||||
|
||||
function resolvePath(...pathes) {
|
||||
return (
|
||||
'/' +
|
||||
pathes
|
||||
.filter((path) => !!path && path !== '/')
|
||||
.map((path) => path.replace(/(^\/)|(\/$)/g, ''))
|
||||
.join('/')
|
||||
)
|
||||
}
|
||||
|
||||
function generateOptions(routes, basePath) {
|
||||
let options = []
|
||||
routes.forEach((route) => {
|
||||
if (route.name && !route.isHidden) {
|
||||
let curOption = {
|
||||
label: (route.meta && route.meta.title) || route.name,
|
||||
key: route.name,
|
||||
path: resolvePath(basePath, route.path),
|
||||
}
|
||||
if (route.children && route.children.length) {
|
||||
curOption.children = generateOptions(route.children, resolvePath(basePath, route.path))
|
||||
}
|
||||
if (curOption.children && curOption.children.length <= 1) {
|
||||
if (curOption.children.length === 1) {
|
||||
curOption = { ...curOption.children[0] }
|
||||
}
|
||||
delete curOption.children
|
||||
}
|
||||
options.push(curOption)
|
||||
}
|
||||
})
|
||||
return options
|
||||
}
|
||||
|
||||
function handleMenuSelect(key, item) {
|
||||
router.push(item.path)
|
||||
|
||||
// 通过path重定向
|
||||
// router.push({
|
||||
// path: '/redirect',
|
||||
// query: { redirect: item.path },
|
||||
// })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-menu
|
||||
class="side-menu"
|
||||
:root-indent="20"
|
||||
:options="menuOptions"
|
||||
:value="(currentRoute.meta && currentRoute.meta.activeMenu) || currentRoute.name"
|
||||
@update:value="handleMenuSelect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.n-menu {
|
||||
margin-top: 10px;
|
||||
padding-left: 10px;
|
||||
.n-menu-item {
|
||||
margin-top: 0;
|
||||
position: relative;
|
||||
&::before {
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 0;
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.n-menu-item--selected {
|
||||
border-radius: 0 !important;
|
||||
|
||||
&::before {
|
||||
border-right: 3px solid $primaryColor;
|
||||
background-color: #16243a;
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba($primaryColor, 0.3) 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.n-menu-item-content-header {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.n-submenu-children {
|
||||
.n-menu-item-content-header {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
position: relative;
|
||||
overflow: visible !important;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -15px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// .side-menu {
|
||||
// // padding-left: 15px;
|
||||
// .n-menu-item-content-header {
|
||||
// color: #fff !important;
|
||||
// font-weight: bold;
|
||||
// font-size: 14px;
|
||||
// }
|
||||
|
||||
// .n-submenu {
|
||||
// .n-menu-item-content-header {
|
||||
// color: #fff !important;
|
||||
// font-weight: bold;
|
||||
// font-size: 14px;
|
||||
// }
|
||||
// }
|
||||
// .n-submenu-children {
|
||||
// .n-menu-item-content-header {
|
||||
// color: #fff !important;
|
||||
// font-weight: normal;
|
||||
// font-size: 12px;
|
||||
// }
|
||||
// }
|
||||
// .n-menu-item {
|
||||
// border-top-left-radius: 5px;
|
||||
// border-bottom-left-radius: 5px;
|
||||
// &:hover,
|
||||
// &.n-menu-item--selected::before {
|
||||
// background-color: #16243a;
|
||||
// right: 0;
|
||||
// left: 0;
|
||||
// border-right: 3px solid $primaryColor;
|
||||
// background-color: unset !important;
|
||||
// background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba($primaryColor, 0.3) 100%);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
</style>
|
9
src/layout/components/sidebar/index.vue
Normal file
9
src/layout/components/sidebar/index.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
import SideLogo from './SideLogo.vue'
|
||||
import SideMenu from './SideMenu.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<side-logo />
|
||||
<side-menu />
|
||||
</template>
|
30
src/layout/index.vue
Normal file
30
src/layout/index.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<script setup>
|
||||
import { NLayout, NLayoutHeader, NLayoutSider } from 'naive-ui'
|
||||
|
||||
import AppHeader from './components/header/index.vue'
|
||||
import SideMenu from './components/sidebar/index.vue'
|
||||
import AppMain from './components/AppMain.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<n-layout has-sider position="absolute">
|
||||
<n-layout-sider :width="200" :collapsed-width="0" :native-scrollbar="false">
|
||||
<side-menu />
|
||||
</n-layout-sider>
|
||||
<n-layout>
|
||||
<n-layout-header style="height: 100px; background-color: #f5f6fb">
|
||||
<app-header />
|
||||
</n-layout-header>
|
||||
<n-layout
|
||||
position="absolute"
|
||||
content-style="padding: 0 35px 35px"
|
||||
style="top: 100px; background-color: #f5f6fb"
|
||||
:native-scrollbar="false"
|
||||
>
|
||||
<app-main />
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</div>
|
||||
</template>
|
19
src/main.js
Normal file
19
src/main.js
Normal file
@ -0,0 +1,19 @@
|
||||
import '@/styles/index.scss'
|
||||
import 'uno.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
import { setupRouter } from '@/router'
|
||||
import { setupStore } from '@/store'
|
||||
|
||||
async function bootstrap() {
|
||||
const app = createApp(App)
|
||||
|
||||
setupStore(app)
|
||||
setupRouter(app)
|
||||
|
||||
app.mount('#app', true)
|
||||
}
|
||||
|
||||
bootstrap()
|
7
src/router/guard/index.js
Normal file
7
src/router/guard/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { createPageLoadingGuard } from './pageLoadingGuard'
|
||||
import { createPermissionGuard } from './permissionGuard'
|
||||
|
||||
export function setupRouterGuard(router) {
|
||||
createPageLoadingGuard(router)
|
||||
createPermissionGuard(router)
|
||||
}
|
15
src/router/guard/pageLoadingGuard.js
Normal file
15
src/router/guard/pageLoadingGuard.js
Normal file
@ -0,0 +1,15 @@
|
||||
export function createPageLoadingGuard(router) {
|
||||
router.beforeEach(() => {
|
||||
$loadingBar.start()
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
setTimeout(() => {
|
||||
$loadingBar.finish()
|
||||
}, 200)
|
||||
})
|
||||
|
||||
router.onError(() => {
|
||||
$loadingBar.error()
|
||||
})
|
||||
}
|
44
src/router/guard/permissionGuard.js
Normal file
44
src/router/guard/permissionGuard.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import { NOT_FOUND_ROUTE } from '@/router/routes'
|
||||
import { getToken, refreshAccessToken, removeToken } from '@/utils/token'
|
||||
|
||||
const WHITE_LIST = ['/login', '/redirect']
|
||||
export function createPermissionGuard(router) {
|
||||
const userStore = useUserStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
if (to.path === '/login') {
|
||||
next({ path: '/' })
|
||||
} else {
|
||||
if (userStore.userId) {
|
||||
// 已经拿到用户信息
|
||||
refreshAccessToken()
|
||||
next()
|
||||
} else {
|
||||
try {
|
||||
await userStore.getUserInfo()
|
||||
const accessRoutes = permissionStore.generateRoutes(userStore.role)
|
||||
accessRoutes.forEach((route) => {
|
||||
!router.hasRoute(route.name) && router.addRoute(route)
|
||||
})
|
||||
router.addRoute(NOT_FOUND_ROUTE)
|
||||
next({ ...to, replace: true })
|
||||
} catch (error) {
|
||||
removeToken()
|
||||
$message.error(error)
|
||||
next({ path: '/login', query: { ...to.query, redirect: to.path } })
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (WHITE_LIST.includes(to.path)) {
|
||||
next()
|
||||
} else {
|
||||
next({ path: '/login', query: { ...to.query, redirect: to.path } })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
23
src/router/index.js
Normal file
23
src/router/index.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { setupRouterGuard } from './guard'
|
||||
import { basicRoutes } from './routes'
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory('/'),
|
||||
routes: basicRoutes,
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
})
|
||||
|
||||
export function resetRouter() {
|
||||
router.getRoutes().forEach((route) => {
|
||||
const { name } = route
|
||||
if (name && !WHITE_NAME_LIST.includes(name)) {
|
||||
router.hasRoute(name) && router.removeRoute(name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function setupRouter(app) {
|
||||
app.use(router)
|
||||
setupRouterGuard(router)
|
||||
}
|
111
src/router/routes/index.js
Normal file
111
src/router/routes/index.js
Normal file
@ -0,0 +1,111 @@
|
||||
import Layout from '@/layout/index.vue'
|
||||
import Dashboard from '@/views/dashboard/index.vue'
|
||||
|
||||
export const basicRoutes = [
|
||||
{
|
||||
name: '404',
|
||||
path: '/404',
|
||||
component: () => import('@/views/error-page/404.vue'),
|
||||
isHidden: true,
|
||||
},
|
||||
{
|
||||
name: '401',
|
||||
path: '/401',
|
||||
component: () => import('@/views/error-page/401.vue'),
|
||||
isHidden: true,
|
||||
},
|
||||
{
|
||||
name: 'REDIRECT',
|
||||
path: '/redirect',
|
||||
component: Layout,
|
||||
isHidden: true,
|
||||
children: [
|
||||
{
|
||||
name: 'REDIRECT_NAME',
|
||||
path: '',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'LOGIN',
|
||||
path: '/login',
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
isHidden: true,
|
||||
meta: {
|
||||
title: '登录页',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'HOME',
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/dashboard',
|
||||
meta: {
|
||||
title: '首页',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'DASHBOARD',
|
||||
path: 'dashboard',
|
||||
component: Dashboard,
|
||||
meta: {
|
||||
title: 'Dashboard',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'TEST',
|
||||
path: '/test',
|
||||
component: Layout,
|
||||
redirect: '/test/unocss',
|
||||
meta: {
|
||||
title: '测试',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'UNOCSS',
|
||||
path: 'unocss',
|
||||
component: () => import('@/views/test-page/TestUnocss.vue'),
|
||||
meta: {
|
||||
title: '测试unocss',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'MESSAGE',
|
||||
path: 'message',
|
||||
component: () => import('@/views/test-page/TestMessage.vue'),
|
||||
meta: {
|
||||
title: '测试Message',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'DIALOG',
|
||||
path: 'dialog',
|
||||
component: () => import('@/views/test-page/TestDialog.vue'),
|
||||
meta: {
|
||||
title: '测试Dialog',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const NOT_FOUND_ROUTE = {
|
||||
name: 'NOT_FOUND',
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/404',
|
||||
isHidden: true,
|
||||
}
|
||||
|
||||
const modules = import.meta.globEager('./modules/*.js')
|
||||
const asyncRoutes = []
|
||||
Object.keys(modules).forEach((key) => {
|
||||
asyncRoutes.push(...modules[key].default)
|
||||
})
|
||||
|
||||
export { asyncRoutes }
|
34
src/router/routes/modules/user.js
Normal file
34
src/router/routes/modules/user.js
Normal file
@ -0,0 +1,34 @@
|
||||
import Layout from '@/layout/index.vue'
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'USER_MANAGER',
|
||||
path: '/user',
|
||||
component: Layout,
|
||||
redirect: '/user/management',
|
||||
meta: {
|
||||
title: '用户中心',
|
||||
role: ['admin'],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'USER',
|
||||
path: 'management',
|
||||
component: () => import('@/views/user/index.vue'),
|
||||
meta: {
|
||||
title: '用户管理',
|
||||
role: ['admin'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'PERMISSION',
|
||||
path: 'permission',
|
||||
component: () => import('@/views/user/UserPermission.vue'),
|
||||
meta: {
|
||||
title: '权限管理',
|
||||
role: ['admin'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
5
src/store/index.js
Normal file
5
src/store/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
export function setupStore(app) {
|
||||
app.use(createPinia())
|
||||
}
|
17
src/store/modules/app.js
Normal file
17
src/store/modules/app.js
Normal file
@ -0,0 +1,17 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state() {
|
||||
return {
|
||||
themeOverrides: {
|
||||
common: {
|
||||
primaryColor: '#316c72',
|
||||
primaryColorSuppl: '#316c72',
|
||||
primaryColorHover: '#316c72',
|
||||
successColorHover: '#316c72',
|
||||
successColorSuppl: '#316c72',
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
50
src/store/modules/permission.js
Normal file
50
src/store/modules/permission.js
Normal file
@ -0,0 +1,50 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { asyncRoutes, basicRoutes } from '@/router/routes'
|
||||
import { difference } from 'lodash-es'
|
||||
|
||||
function hasPermission(route, role) {
|
||||
const routeRole = route.meta?.role ? route.meta.role : []
|
||||
if (!role.length || !routeRole.length) {
|
||||
return false
|
||||
}
|
||||
return difference(routeRole, role).length < routeRole.length
|
||||
}
|
||||
|
||||
function filterAsyncRoutes(routes = [], role) {
|
||||
const ret = []
|
||||
routes.forEach((route) => {
|
||||
if (hasPermission(route, role)) {
|
||||
const curRoute = {
|
||||
...route,
|
||||
children: [],
|
||||
}
|
||||
if (route.children && route.children.length) {
|
||||
curRoute.children = filterAsyncRoutes(route.children, role)
|
||||
} else {
|
||||
Reflect.deleteProperty(curRoute, 'children')
|
||||
}
|
||||
ret.push(curRoute)
|
||||
}
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
export const usePermissionStore = defineStore('permission', {
|
||||
state() {
|
||||
return {
|
||||
accessRoutes: [],
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
routes() {
|
||||
return basicRoutes.concat(this.accessRoutes)
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
generateRoutes(role = []) {
|
||||
const accessRoutes = filterAsyncRoutes(asyncRoutes, role)
|
||||
this.accessRoutes = accessRoutes
|
||||
return accessRoutes
|
||||
},
|
||||
},
|
||||
})
|
46
src/store/modules/user.js
Normal file
46
src/store/modules/user.js
Normal file
@ -0,0 +1,46 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { getUser } from '@/api/user'
|
||||
import { removeToken } from '@/utils/token'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state() {
|
||||
return {
|
||||
userInfo: {},
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
userId() {
|
||||
return this.userInfo?.id
|
||||
},
|
||||
name() {
|
||||
return this.userInfo?.name
|
||||
},
|
||||
avatar() {
|
||||
return this.userInfo?.avatar
|
||||
},
|
||||
role() {
|
||||
return this.userInfo?.role || []
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async getUserInfo() {
|
||||
try {
|
||||
const res = await getUser()
|
||||
if (res.code === 0) {
|
||||
const { id, name, avatar, role } = res.data
|
||||
this.userInfo = { id, name, avatar, role }
|
||||
return Promise.resolve(res.data)
|
||||
} else {
|
||||
return Promise.reject(res.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return Promise.reject(error.message)
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
removeToken()
|
||||
this.userInfo = {}
|
||||
},
|
||||
},
|
||||
})
|
2
src/styles/index.scss
Normal file
2
src/styles/index.scss
Normal file
@ -0,0 +1,2 @@
|
||||
@import './reset.scss';
|
||||
@import './public.scss';
|
28
src/styles/public.scss
Normal file
28
src/styles/public.scss
Normal file
@ -0,0 +1,28 @@
|
||||
html {
|
||||
font-size: 4px; // * 1rem = 4px 方便unocss计算:在unocss中 1字体单位 = 0.25rem,相当于 1等份 = 1px
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #f2f2f2;
|
||||
font-family: 'Encode Sans Condensed', sans-serif;
|
||||
}
|
||||
|
||||
/* router view transition fade-slide */
|
||||
.fade-slide-leave-active,
|
||||
.fade-slide-enter-active {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.fade-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.fade-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
40
src/styles/reset.scss
Normal file
40
src/styles/reset.scss
Normal file
@ -0,0 +1,40 @@
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:link,
|
||||
a:visited,
|
||||
a:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
outline: none;
|
||||
border: none;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
5
src/styles/variables.scss
Normal file
5
src/styles/variables.scss
Normal file
@ -0,0 +1,5 @@
|
||||
$primaryColor: #316c72;
|
||||
|
||||
:root {
|
||||
--vh100: 100vh;
|
||||
}
|
9
src/utils/cache/index.js
vendored
Normal file
9
src/utils/cache/index.js
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
import { createWebStorage } from './webStorage'
|
||||
|
||||
export const createLocalStorage = function (option = {}) {
|
||||
return createWebStorage({ prefixKey: option.prefixKey || '', storage: localStorage })
|
||||
}
|
||||
|
||||
export const createSessionStorage = function (option = {}) {
|
||||
return createWebStorage({ prefixKey: option.prefixKey || '', storage: localStorage })
|
||||
}
|
55
src/utils/cache/webStorage.js
vendored
Normal file
55
src/utils/cache/webStorage.js
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
import { isNullOrUndef } from '@/utils/is'
|
||||
|
||||
class WebStorage {
|
||||
constructor(option) {
|
||||
this.storage = option.storage
|
||||
this.prefixKey = option.prefixKey
|
||||
}
|
||||
|
||||
getKey(key) {
|
||||
return `${this.prefixKey}${key}`.toUpperCase()
|
||||
}
|
||||
|
||||
set(key, value, expire) {
|
||||
const stringData = JSON.stringify({
|
||||
value,
|
||||
time: Date.now(),
|
||||
expire: !isNullOrUndef(expire) ? new Date().getTime() + expire * 1000 : null,
|
||||
})
|
||||
this.storage.setItem(this.getKey(key), stringData)
|
||||
}
|
||||
|
||||
get(key) {
|
||||
const { value } = this.getItem(key, {})
|
||||
return value
|
||||
}
|
||||
|
||||
getItem(key, def = null) {
|
||||
const val = this.storage.getItem(this.getKey(key))
|
||||
if (!val) return def
|
||||
try {
|
||||
const data = JSON.parse(val)
|
||||
const { value, time, expire } = data
|
||||
if (isNullOrUndef(expire) || expire > new Date().getTime()) {
|
||||
return { value, time }
|
||||
}
|
||||
this.remove(key)
|
||||
return def
|
||||
} catch (error) {
|
||||
this.remove(key)
|
||||
return def
|
||||
}
|
||||
}
|
||||
|
||||
remove(key) {
|
||||
this.storage.removeItem(this.getKey(key))
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.storage.clear()
|
||||
}
|
||||
}
|
||||
|
||||
export function createWebStorage({ prefixKey = '', storage = sessionStorage }) {
|
||||
return new WebStorage({ prefixKey, storage })
|
||||
}
|
13
src/utils/http/help.js
Normal file
13
src/utils/http/help.js
Normal file
@ -0,0 +1,13 @@
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
const WITHOUT_TOKEN_API = [{ url: '/auth/login', method: 'POST' }]
|
||||
|
||||
export function isWithoutToken({ url, method = '' }) {
|
||||
return WITHOUT_TOKEN_API.some((item) => item.url === url && item.method === method.toUpperCase())
|
||||
}
|
||||
|
||||
export function addBaseParams(params) {
|
||||
if (!params.userId) {
|
||||
params.userId = useUserStore().userId
|
||||
}
|
||||
}
|
18
src/utils/http/index.js
Normal file
18
src/utils/http/index.js
Normal file
@ -0,0 +1,18 @@
|
||||
import axios from 'axios'
|
||||
import { setupInterceptor } from './interceptors'
|
||||
|
||||
function createAxios(option = {}) {
|
||||
const defBaseURL = window.__APP__GLOB__CONF__?.VITE_APP_GLOB_BASE_API || import.meta.env.VITE_APP_GLOB_BASE_API
|
||||
const service = axios.create({
|
||||
timeout: option.timeout || 120000,
|
||||
baseURL: option.baseURL || defBaseURL,
|
||||
})
|
||||
setupInterceptor(service)
|
||||
return service
|
||||
}
|
||||
|
||||
export const defAxios = createAxios()
|
||||
|
||||
export const mockAxios = createAxios({
|
||||
baseURL: window.__APP__GLOB__CONF__?.VITE_APP_GLOB_BASE_API_TEST || import.meta.env.VITE_APP_GLOB_BASE_API_TEST,
|
||||
})
|
82
src/utils/http/interceptors.js
Normal file
82
src/utils/http/interceptors.js
Normal file
@ -0,0 +1,82 @@
|
||||
import { router } from '@/router'
|
||||
import { getToken, removeToken } from '@/utils/token'
|
||||
import { isWithoutToken } from './help'
|
||||
|
||||
export function setupInterceptor(service) {
|
||||
service.interceptors.request.use(
|
||||
async (config) => {
|
||||
// 防止缓存,给get请求加上时间戳
|
||||
if (config.method === 'get') {
|
||||
config.params = { ...config.params, t: new Date().getTime() }
|
||||
}
|
||||
|
||||
// 处理不需要token的请求
|
||||
if (isWithoutToken(config)) {
|
||||
return config
|
||||
}
|
||||
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
/**
|
||||
* * jwt token
|
||||
* ! 认证方案: Bearer
|
||||
*/
|
||||
config.headers.Authorization = 'Bearer ' + token
|
||||
|
||||
return config
|
||||
}
|
||||
/**
|
||||
* * 未登录或者token过期的情况下
|
||||
* * 跳转登录页重新登录,携带当前路由及参数,登录成功会回到原来的页面
|
||||
*/
|
||||
const { currentRoute } = router
|
||||
router.replace({
|
||||
path: '/login',
|
||||
query: { ...currentRoute.query, redirect: currentRoute.path },
|
||||
})
|
||||
return Promise.reject({ code: '-1', message: '未登录' })
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
|
||||
service.interceptors.response.use(
|
||||
(response) => response?.data,
|
||||
(error) => {
|
||||
let { code, message } = error.response?.data
|
||||
return Promise.reject({ code, message })
|
||||
|
||||
/**
|
||||
* TODO 此处可以根据后端返回的错误码自定义框架层面的错误处理
|
||||
*/
|
||||
switch (code) {
|
||||
case 401:
|
||||
// 未登录(可能是token过期或者无效了)
|
||||
console.error(message)
|
||||
removeToken()
|
||||
const { currentRoute } = router
|
||||
router.replace({
|
||||
path: '/login',
|
||||
query: { ...currentRoute.query, redirect: currentRoute.path },
|
||||
})
|
||||
break
|
||||
case 403:
|
||||
// 没有权限
|
||||
console.error(message)
|
||||
break
|
||||
case 404:
|
||||
// 资源不存在
|
||||
console.error(message)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
// 已知错误resolve,在业务代码中作提醒,未知错误reject,捕获错误统一提示接口异常(9000以上为业务类型错误,需要跟后端确定好)
|
||||
if ([401, 403, 404].includes(code) || code >= 9000) {
|
||||
return Promise.resolve({ code, message })
|
||||
} else {
|
||||
console.error('【err】' + error)
|
||||
return Promise.reject({ message: '接口异常,请稍后重试!' })
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
76
src/utils/index.js
Normal file
76
src/utils/index.js
Normal file
@ -0,0 +1,76 @@
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
/**
|
||||
* @desc 格式化时间
|
||||
* @param {(Object|string|number)} time
|
||||
* @param {string} format
|
||||
* @returns {string | null}
|
||||
*/
|
||||
export function formatDateTime(time = undefined, format = 'YYYY-MM-DD HH:mm:ss') {
|
||||
return dayjs(time).format(format)
|
||||
}
|
||||
|
||||
export function formatDate(date = undefined, format = 'YYYY-MM-DD') {
|
||||
return formatDateTime(date, format)
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc 函数节流
|
||||
* @param {Function} fn
|
||||
* @param {Number} wait
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function throttle(fn, wait) {
|
||||
var context, args
|
||||
var previous = 0
|
||||
|
||||
return function () {
|
||||
var now = +new Date()
|
||||
context = this
|
||||
args = arguments
|
||||
if (now - previous > wait) {
|
||||
fn.apply(context, args)
|
||||
previous = now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc 函数防抖
|
||||
* @param {Function} func
|
||||
* @param {number} wait
|
||||
* @param {boolean} immediate
|
||||
* @return {*}
|
||||
*/
|
||||
export function debounce(method, wait, immediate) {
|
||||
let timeout
|
||||
return function (...args) {
|
||||
let context = this
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
// 立即执行需要两个条件,一是immediate为true,二是timeout未被赋值或被置为null
|
||||
if (immediate) {
|
||||
/**
|
||||
* 如果定时器不存在,则立即执行,并设置一个定时器,wait毫秒后将定时器置为null
|
||||
* 这样确保立即执行后wait毫秒内不会被再次触发
|
||||
*/
|
||||
let callNow = !timeout
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null
|
||||
}, wait)
|
||||
if (callNow) {
|
||||
method.apply(context, args)
|
||||
}
|
||||
} else {
|
||||
// 如果immediate为false,则函数wait毫秒后执行
|
||||
timeout = setTimeout(() => {
|
||||
/**
|
||||
* args是一个类数组对象,所以使用fn.apply
|
||||
* 也可写作method.call(context, ...args)
|
||||
*/
|
||||
method.apply(context, args)
|
||||
}, wait)
|
||||
}
|
||||
}
|
||||
}
|
91
src/utils/is.js
Normal file
91
src/utils/is.js
Normal file
@ -0,0 +1,91 @@
|
||||
const toString = Object.prototype.toString
|
||||
|
||||
export function is(val, type) {
|
||||
return toString.call(val) === `[object ${type}]`
|
||||
}
|
||||
|
||||
export function isDef(val) {
|
||||
return typeof val !== 'undefined'
|
||||
}
|
||||
|
||||
export function isUndef(val) {
|
||||
return typeof val === 'undefined'
|
||||
}
|
||||
|
||||
export function isNull(val) {
|
||||
return val === null
|
||||
}
|
||||
|
||||
export function isObject(val) {
|
||||
return !isNull(isNull) && is(val, 'Object')
|
||||
}
|
||||
|
||||
export function isArray(val) {
|
||||
return val && Array.isArray(val)
|
||||
}
|
||||
|
||||
export function isString(val) {
|
||||
return is(val, 'String')
|
||||
}
|
||||
|
||||
export function isNumber(val) {
|
||||
return is(val, 'Number')
|
||||
}
|
||||
|
||||
export function isBoolean(val) {
|
||||
return is(val, 'Boolean')
|
||||
}
|
||||
|
||||
export function isDate(val) {
|
||||
return is(val, 'Date')
|
||||
}
|
||||
|
||||
export function isRegExp(val) {
|
||||
return is(val, 'RegExp')
|
||||
}
|
||||
|
||||
export function isFunction(val) {
|
||||
return typeof val === 'function'
|
||||
}
|
||||
|
||||
export function isPromise(val) {
|
||||
return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch)
|
||||
}
|
||||
|
||||
export function isElement(val) {
|
||||
return isObject(val) && !!val.tagName
|
||||
}
|
||||
|
||||
export function isWindow(val) {
|
||||
return typeof window !== 'undefined' && isDef(window) && is(val, 'Window')
|
||||
}
|
||||
|
||||
export function isNullOrUndef(val) {
|
||||
return isNull(val) || isUndef(val)
|
||||
}
|
||||
|
||||
export function isEmpty(val) {
|
||||
if (isArray(val) || isString(val)) {
|
||||
return val.length === 0
|
||||
}
|
||||
|
||||
if (val instanceof Map || val instanceof Set) {
|
||||
return val.size === 0
|
||||
}
|
||||
|
||||
if (isObject(val)) {
|
||||
return Object.keys(val).length === 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function isUrl(path) {
|
||||
const reg =
|
||||
/(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/
|
||||
return reg.test(path)
|
||||
}
|
||||
|
||||
export const isServer = typeof window === 'undefined'
|
||||
|
||||
export const isClient = !isServer
|
37
src/utils/token.js
Normal file
37
src/utils/token.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { createLocalStorage } from './cache'
|
||||
import { refreshToken } from '@/api/auth'
|
||||
|
||||
const TOKEN_CODE = 'access_token'
|
||||
const DURATION = 6 * 60 * 60
|
||||
|
||||
export const lsToken = createLocalStorage()
|
||||
|
||||
export function getToken() {
|
||||
return lsToken.get(TOKEN_CODE)
|
||||
}
|
||||
|
||||
export function setToken(token) {
|
||||
lsToken.set(TOKEN_CODE, token, DURATION)
|
||||
}
|
||||
|
||||
export function removeToken() {
|
||||
lsToken.remove(TOKEN_CODE)
|
||||
}
|
||||
|
||||
export async function refreshAccessToken() {
|
||||
const tokenItem = lsToken.getItem(TOKEN_CODE)
|
||||
if (!tokenItem) {
|
||||
return
|
||||
}
|
||||
const { time } = tokenItem
|
||||
if (new Date().getTime() - time > 1000 * 60 * 30) {
|
||||
try {
|
||||
const res = await refreshToken()
|
||||
if (res.code === 0) {
|
||||
setToken(res.data.token)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
3
src/views/dashboard/index.vue
Normal file
3
src/views/dashboard/index.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<h1>首页</h1>
|
||||
</template>
|
3
src/views/error-page/401.vue
Normal file
3
src/views/error-page/401.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<h1>401</h1>
|
||||
</template>
|
3
src/views/error-page/404.vue
Normal file
3
src/views/error-page/404.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<h1>404</h1>
|
||||
</template>
|
207
src/views/login/index.vue
Normal file
207
src/views/login/index.vue
Normal file
@ -0,0 +1,207 @@
|
||||
<script setup>
|
||||
import { ref, unref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { login } from '@/api/auth'
|
||||
import { createLocalStorage } from '@/utils/cache'
|
||||
import { setToken } from '@/utils/token'
|
||||
|
||||
const title = import.meta.env.VITE_APP_TITLE
|
||||
|
||||
const router = useRouter()
|
||||
const query = unref(router.currentRoute).query
|
||||
|
||||
const loginInfo = ref({
|
||||
name: 'admin',
|
||||
password: 123456,
|
||||
})
|
||||
|
||||
const ls = createLocalStorage({ prefixKey: 'login_' })
|
||||
const lsLoginInfo = ls.get('loginInfo')
|
||||
if (lsLoginInfo) {
|
||||
loginInfo.value.name = lsLoginInfo.name || ''
|
||||
loginInfo.value.password = lsLoginInfo.password || ''
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
const { name, password } = loginInfo.value
|
||||
if (!name || !password) {
|
||||
$message.warning('请输入用户名和密码')
|
||||
return
|
||||
}
|
||||
try {
|
||||
$message.loading('正在验证...')
|
||||
const res = await login({ name, password: password.toString() })
|
||||
if (res.code === 0) {
|
||||
$message.success('登录成功')
|
||||
ls.set('loginInfo', { name, password })
|
||||
setToken(res.data.token)
|
||||
|
||||
if (query.redirect) {
|
||||
router.push({ path: '/redirect', query })
|
||||
} else {
|
||||
router.push('/')
|
||||
}
|
||||
} else {
|
||||
$message.warning(res.message)
|
||||
}
|
||||
} catch (error) {
|
||||
$message.error(error.message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="form-wrapper">
|
||||
<h2 class="title">{{ title }}</h2>
|
||||
<div class="form-item" mt-20>
|
||||
<input
|
||||
v-model="loginInfo.name"
|
||||
autofocus
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="username"
|
||||
@keydown.enter="handleLogin"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item" mt-20>
|
||||
<input
|
||||
v-model="loginInfo.password"
|
||||
type="password"
|
||||
class="input"
|
||||
placeholder="password"
|
||||
@keydown.enter="handleLogin"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item" mt-20>
|
||||
<button class="submit-btn" @click="handleLogin">登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@property --perA {
|
||||
syntax: '<percentage>';
|
||||
inherits: false;
|
||||
initial-value: 75%;
|
||||
}
|
||||
|
||||
@property --perB {
|
||||
syntax: '<percentage>';
|
||||
inherits: false;
|
||||
initial-value: 99%;
|
||||
}
|
||||
|
||||
@property --perC {
|
||||
syntax: '<percentage>';
|
||||
inherits: false;
|
||||
initial-value: 15%;
|
||||
}
|
||||
|
||||
@property --perD {
|
||||
syntax: '<percentage>';
|
||||
inherits: false;
|
||||
initial-value: 16%;
|
||||
}
|
||||
|
||||
@property --perE {
|
||||
syntax: '<percentage>';
|
||||
inherits: false;
|
||||
initial-value: 86%;
|
||||
}
|
||||
|
||||
@property --angle {
|
||||
syntax: '<angle>';
|
||||
inherits: false;
|
||||
initial-value: 0deg;
|
||||
}
|
||||
.login-page {
|
||||
height: 100%;
|
||||
background-color: #e1e8ee;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-image: radial-gradient(
|
||||
circle at var(--perE) 7%,
|
||||
rgba(40, 40, 40, 0.04) 0%,
|
||||
rgba(40, 40, 40, 0.04) 50%,
|
||||
rgba(200, 200, 200, 0.04) 50%,
|
||||
rgba(200, 200, 200, 0.04) 100%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at var(--perC) var(--perD),
|
||||
rgba(99, 99, 99, 0.04) 0%,
|
||||
rgba(99, 99, 99, 0.04) 50%,
|
||||
rgba(45, 45, 45, 0.04) 50%,
|
||||
rgba(45, 45, 45, 0.04) 100%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at var(--perA) var(--perB),
|
||||
rgba(243, 243, 243, 0.04) 0%,
|
||||
rgba(243, 243, 243, 0.04) 50%,
|
||||
rgba(37, 37, 37, 0.04) 50%,
|
||||
rgba(37, 37, 37, 0.04) 100%
|
||||
),
|
||||
linear-gradient(var(--angle), #22deed, #8759d7);
|
||||
animation: move 30s infinite alternate linear;
|
||||
|
||||
@keyframes move {
|
||||
100% {
|
||||
--perA: 85%;
|
||||
--perB: 49%;
|
||||
--perC: 45%;
|
||||
--perD: 39%;
|
||||
--perE: 70%;
|
||||
--angle: 360deg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-wrapper {
|
||||
text-align: center;
|
||||
padding: 40px 50px;
|
||||
border-radius: 15px;
|
||||
background-color: rgba(#fff, 0.2);
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
color: #f3f3f3;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
width: 240px;
|
||||
input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 15px;
|
||||
border-radius: 5px;
|
||||
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
transition: 0.3s;
|
||||
&:focus {
|
||||
box-shadow: 0 0 5px #8759d7;
|
||||
}
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
background-color: #6683d2;
|
||||
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
21
src/views/redirect/index.vue
Normal file
21
src/views/redirect/index.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { currentRoute, replace } = useRouter()
|
||||
|
||||
const { query } = currentRoute.value
|
||||
let { redirect } = query
|
||||
Reflect.deleteProperty(query, 'redirect')
|
||||
|
||||
if (Array.isArray(redirect)) {
|
||||
redirect = redirect.join('/')
|
||||
}
|
||||
if (redirect.startsWith('/redirect')) {
|
||||
redirect = '/'
|
||||
}
|
||||
|
||||
replace({
|
||||
path: redirect.startsWith('/') ? redirect : '/' + redirect,
|
||||
query,
|
||||
})
|
||||
</script>
|
18
src/views/test-page/TestDialog.vue
Normal file
18
src/views/test-page/TestDialog.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<script setup>
|
||||
import { NButton } from 'naive-ui'
|
||||
const handleDelete = function () {
|
||||
$dialog.confirm({
|
||||
content: '确认删除?',
|
||||
confirm() {
|
||||
$dialog.success('删除成功', { positiveText: '我知道了' })
|
||||
},
|
||||
cancel() {
|
||||
$dialog.warning('已取消', { closable: true })
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-button @click="handleDelete">删除</n-button>
|
||||
</template>
|
18
src/views/test-page/TestMessage.vue
Normal file
18
src/views/test-page/TestMessage.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<script setup>
|
||||
import { NButton } from 'naive-ui'
|
||||
|
||||
function handleLogin() {
|
||||
$message.loading('登陆中...')
|
||||
setTimeout(() => {
|
||||
$message.error('登陆失败')
|
||||
$message.loading('正在尝试重新登陆...')
|
||||
setTimeout(() => {
|
||||
$message.success('登陆成功')
|
||||
}, 2000)
|
||||
}, 2000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-button @click="handleLogin">点击登陆</n-button>
|
||||
</template>
|
56
src/views/test-page/TestUnocss.vue
Normal file
56
src/views/test-page/TestUnocss.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="content-box">
|
||||
<p text-12>测试12px</p>
|
||||
<p text-13>测试13px</p>
|
||||
<p text-14>测试14px</p>
|
||||
<p text-15>测试15px</p>
|
||||
<p text-16>测试16px</p>
|
||||
<p text-17>测试17px</p>
|
||||
<p text-18>测试18px</p>
|
||||
<p text-19>测试19px</p>
|
||||
<p text-20>测试20px</p>
|
||||
</div>
|
||||
<div mt-30 class="content-box">
|
||||
<p text-12>测试12px</p>
|
||||
<p text-13>测试13px</p>
|
||||
<p text-14>测试14px</p>
|
||||
<p text-15>测试15px</p>
|
||||
<p text-16>测试16px</p>
|
||||
<p text-17>测试17px</p>
|
||||
<p text-18>测试18px</p>
|
||||
<p text-19>测试19px</p>
|
||||
<p text-20>测试20px</p>
|
||||
</div>
|
||||
<div mt-30 class="content-box">
|
||||
<p text-12>测试12px</p>
|
||||
<p text-13>测试13px</p>
|
||||
<p text-14>测试14px</p>
|
||||
<p text-15>测试15px</p>
|
||||
<p text-16>测试16px</p>
|
||||
<p text-17>测试17px</p>
|
||||
<p text-18>测试18px</p>
|
||||
<p text-19>测试19px</p>
|
||||
<p text-20>测试20px</p>
|
||||
</div>
|
||||
<div mt-30 class="content-box">
|
||||
<p text-12>测试12px</p>
|
||||
<p text-13>测试13px</p>
|
||||
<p text-14>测试14px</p>
|
||||
<p text-15>测试15px</p>
|
||||
<p text-16>测试16px</p>
|
||||
<p text-17>测试17px</p>
|
||||
<p text-18>测试18px</p>
|
||||
<p text-19>测试19px</p>
|
||||
<p text-20>测试20px</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content-box {
|
||||
background-color: #fff;
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
3
src/views/user/UserPermission.vue
Normal file
3
src/views/user/UserPermission.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<h1>权限管理</h1>
|
||||
</template>
|
5
src/views/user/index.vue
Normal file
5
src/views/user/index.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
<h1>用户管理</h1>
|
||||
</template>
|
46
vite.config.js
Normal file
46
vite.config.js
Normal file
@ -0,0 +1,46 @@
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import path from 'path'
|
||||
|
||||
import { wrapperEnv } from './build/utils'
|
||||
import { createVitePlugins } from './build/vite/plugin'
|
||||
import { createProxy } from './build/vite/proxy'
|
||||
import { OUTPUT_DIR } from './build/constant'
|
||||
|
||||
export default defineConfig(({ command, mode }) => {
|
||||
const root = process.cwd()
|
||||
const isBuild = command === 'build'
|
||||
|
||||
const env = loadEnv(mode, process.cwd())
|
||||
const viteEnv = wrapperEnv(env)
|
||||
const { VITE_PORT, VITE_PUBLIC_PATH, VITE_PROXY } = viteEnv
|
||||
|
||||
return {
|
||||
root,
|
||||
base: VITE_PUBLIC_PATH || '/',
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
plugins: createVitePlugins(viteEnv, isBuild),
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
//define global scss variable
|
||||
scss: {
|
||||
additionalData: `@import '@/styles/variables.scss';`,
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: VITE_PORT,
|
||||
proxy: createProxy(VITE_PROXY),
|
||||
},
|
||||
build: {
|
||||
target: 'es2015',
|
||||
outDir: OUTPUT_DIR,
|
||||
brotliSize: false,
|
||||
chunkSizeWarningLimit: 2000,
|
||||
},
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue
Block a user