Quellcode durchsuchen

commit message

master
zhangtao vor 2 Jahren
Commit
a62eb480b3
90 geänderte Dateien mit 21397 neuen und 0 gelöschten Zeilen
  1. +5
    -0
      .env
  2. +11
    -0
      .env.development
  3. +14
    -0
      .env.localhost
  4. +11
    -0
      .env.production
  5. +11
    -0
      .env.test
  6. +329
    -0
      .eslintrc.js
  7. +5
    -0
      .gitignore
  8. +3
    -0
      .vscode/extensions.json
  9. +7
    -0
      README.md
  10. +3
    -0
      build/constant.js
  11. +14
    -0
      build/script/build-cname.js
  12. +29
    -0
      build/script/build-config.js
  13. +16
    -0
      build/script/index.js
  14. +71
    -0
      build/utils.js
  15. +31
    -0
      build/vite/plugin/html.js
  16. +26
    -0
      build/vite/plugin/index.js
  17. +14
    -0
      build/vite/plugin/mock.js
  18. +9
    -0
      build/vite/plugin/unocss.js
  19. +17
    -0
      build/vite/proxy.js
  20. +17
    -0
      index.html
  21. +9
    -0
      jsconfig.json
  22. +14
    -0
      mock/_create-prod-server.js
  23. +10
    -0
      mock/_util.js
  24. +17
    -0
      mock/login/index.js
  25. +88
    -0
      mock/system/index.js
  26. +41
    -0
      mock/system/router.js
  27. +17822
    -0
      package-lock.json
  28. +44
    -0
      package.json
  29. BIN
      public/favicon.ico
  30. +24
    -0
      src/App.vue
  31. +16
    -0
      src/api/auth/index.js
  32. +15
    -0
      src/api/login/index.js
  33. +39
    -0
      src/api/post/index.js
  34. +23
    -0
      src/api/system/index.js
  35. +38
    -0
      src/api/user/index.js
  36. BIN
      src/assets/logo.png
  37. +24
    -0
      src/components/AppProvider/index.vue
  38. +22
    -0
      src/components/Bread/index.vue
  39. +72
    -0
      src/components/DataTable/index.vue
  40. +71
    -0
      src/components/DataTable/tools/Action.vue
  41. +30
    -0
      src/components/DataTable/tools/Image.vue
  42. +134
    -0
      src/components/DataTable_back/index.vue
  43. +35
    -0
      src/components/DataTable_back/tools/props.js
  44. +99
    -0
      src/components/DataTable_back/tools/useDataSource.js
  45. +34
    -0
      src/components/DataTable_back/tools/usePagination.js
  46. +52
    -0
      src/components/Dialog/index.vue
  47. +17
    -0
      src/components/LoadingBar/index.vue
  48. +72
    -0
      src/components/Message/index.vue
  49. +15
    -0
      src/layout/components/Header/index.vue
  50. +71
    -0
      src/layout/components/Menu/index.vue
  51. +23
    -0
      src/layout/components/Sidebar/index.vue
  52. +250
    -0
      src/layout/components/Tags/index.vue
  53. +43
    -0
      src/layout/index.vue
  54. +19
    -0
      src/main.js
  55. +9
    -0
      src/router/guard/index.js
  56. +15
    -0
      src/router/guard/page-loading-guard.js
  57. +12
    -0
      src/router/guard/page-title-guard.js
  58. +42
    -0
      src/router/guard/permission-guard.js
  59. +24
    -0
      src/router/index.js
  60. +48
    -0
      src/router/routes/index.js
  61. +23
    -0
      src/router/routes/modules/system.js
  62. +25
    -0
      src/setting/config.js
  63. +5
    -0
      src/store/index.js
  64. +17
    -0
      src/store/modules/app.js
  65. +111
    -0
      src/store/modules/permission.js
  66. +25
    -0
      src/store/modules/setting.js
  67. +56
    -0
      src/store/modules/tagsMenu.js
  68. +49
    -0
      src/store/modules/user.js
  69. +3
    -0
      src/styles/index.scss
  70. +12
    -0
      src/styles/layout.scss
  71. +56
    -0
      src/styles/public.scss
  72. +40
    -0
      src/styles/reset.scss
  73. +5
    -0
      src/styles/variables.scss
  74. +9
    -0
      src/utils/cache/index.js
  75. +55
    -0
      src/utils/cache/web-storage.js
  76. +13
    -0
      src/utils/http/help.js
  77. +18
    -0
      src/utils/http/index.js
  78. +82
    -0
      src/utils/http/interceptors.js
  79. +76
    -0
      src/utils/index.js
  80. +110
    -0
      src/utils/is.js
  81. +17
    -0
      src/utils/module.js
  82. +18
    -0
      src/utils/tags.js
  83. +41
    -0
      src/utils/token.js
  84. +12
    -0
      src/utils/ui/theme.js
  85. +26
    -0
      src/views/dashboard/index.vue
  86. +43
    -0
      src/views/login/index.vue
  87. +146
    -0
      src/views/system/menu/index.vue
  88. +17
    -0
      src/views/system/role/index.vue
  89. +162
    -0
      src/views/system/user/index.vue
  90. +49
    -0
      vite.config.js

+ 5
- 0
.env Datei anzeigen

@@ -0,0 +1,5 @@
# title
VITE_APP_TITLE = '河湖长眼'

# 端口号
VITE_PORT = 3000

+ 11
- 0
.env.development Datei anzeigen

@@ -0,0 +1,11 @@
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/'

# 是否启用MOCK
VITE_APP_USE_MOCK = false

# proxy
VITE_PROXY = [["/api-dev","http://127.0.0.1:8002/api"]]

# base api
VITE_APP_GLOB_BASE_API = '/api-dev'

+ 14
- 0
.env.localhost Datei anzeigen

@@ -0,0 +1,14 @@
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/'

# 是否启用MOCK
VITE_APP_USE_MOCK = true

# proxy
VITE_PROXY = [["/api-local","http://127.0.0.1:8002/api"],["/api-mock","http://127.0.0.1:8003"]]

# base api
VITE_APP_GLOB_BASE_API = '/api-local'

# mock base api
VITE_APP_GLOB_BASE_API_MOCK = '/api-mock'

+ 11
- 0
.env.production Datei anzeigen

@@ -0,0 +1,11 @@
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/'

# 是否启用MOCK
VITE_APP_USE_MOCK = false

# proxy
VITE_PROXY = [["/api-prod","http://127.0.0.1:8002/api"]]

# base api
VITE_APP_GLOB_BASE_API = '/api-prod'

+ 11
- 0
.env.test Datei anzeigen

@@ -0,0 +1,11 @@
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/'

# 是否启用MOCK
VITE_APP_USE_MOCK = false

# proxy
VITE_PROXY = [["/api-test","http://127.0.0.1:8002/api/"]]

# base api
VITE_APP_GLOB_BASE_API = '/api-test'

+ 329
- 0
.eslintrc.js Datei anzeigen

@@ -0,0 +1,329 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
es6: true
},
extends: [
'plugin:vue/vue3-recommended',
'eslint:recommended'
],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
/* 强制每行的最大属性 */
'vue/max-attributes-per-line': [2, {
/* 当开始标签位于单行时,每行的最大属性数 */
'singleline': 10,
/* 当开始标签位于多行时,每行的最大属性数 */
'multiline': {
'max': 1
}
}],
/* 在单行元素的内容之前和之后需要换行符 */
'vue/singleline-html-element-content-newline': 'off',
/* 在单行元素的内容之前和之后需要换行符 */
'vue/multiline-html-element-content-newline': 'off',
/* 组件名称驼峰 */
'vue/component-definition-name-casing': ['error', 'PascalCase'],
/* 禁止使用 v-html */
'vue/no-v-html': 'off',
/* 格式化箭头函数的箭头前后空格 */
'arrow-spacing': [2, {
'before': true,
'after': true
}],
/* 强制缩进 */
'block-spacing': [2, 'always'],
/* 强制执行一个真正的大括号风格 */
'brace-style': [2, '1tbs', {
/* 允许一个块打开和关闭括号在同一行上 */
'allowSingleLine': true
}],
/* 为属性名称强制执行 camelcase 样式 */
'camelcase': [0, {
'properties': 'always'
}],
/* 不允许对象和数组尾随逗号 */
'comma-dangle': [2, 'never'],
/* 对象逗号前后允许空格 */
'comma-spacing': [2, {
'before': false,
'after': true
}],
/* 在数组元素、对象属性或变量声明之后和同一行上需要逗号 */
'comma-style': [2, 'last'],
/* 检查是否存在有效的super()调用 */
'constructor-super': 2,
/* 以允许支柱少单行if,else if,else,for,while,或do,同时还规定使用其他实例花括号 */
'curly': [2, 'multi-line'],
/* 成员表达式中的点应与属性部分位于同一行 */
'dot-location': [2, 'property'],
/* 在非空文件的末尾至少执行一个换行符 */
'eol-last': 2,
/* 强制使用===和!== */
'eqeqeq': ['error', 'always'],
/* 强化*发生器功能的间距 */
'generator-star-spacing': [2, {
'before': true,
'after': true
}],
/* 使用回调模式时会处理这个错误 */
'handle-callback-err': [2, '^(err|error)$'],
/* 强制执行一致的缩进样式 */
'indent': [2, 2, {
/* 强制缩进级别case的条款switch声明 */
'SwitchCase': 1
}],
/* 一致使用单引号 */
'jsx-quotes': [2, 'prefer-single'],
/* 强制在对象字面量属性中的键和值之间保持一致的间距 */
'key-spacing': [2, {
/* 不允许在对象文字中的键和冒号之间使用空格 */
'beforeColon': false,
/* 需要在冒号和对象文字中的值之间至少有一个空格 */
'afterColon': true
}],
/* 强制执行围绕关键字和关键字标记的一致空格 */
'keyword-spacing': [2, {
'before': true,
'after': true
}],
/* 要求构造函数名以大写字母开头 */
'new-cap': [2, {
/* 要求new使用大写启动函数调用所有操作符 */
'newIsCap': true,
/* 允许在没有new操作符的情况下调用大写启动的函数 */
'capIsNew': false
}],
/* 在使用new关键字调用不带参数的构造函数时需要括号,以便提高代码清晰度 */
'new-parens': 2,
/* 不允许使用Array构造函数 */
'no-array-constructor': 2,
/* 阻止使用已弃用和次优代码 */
'no-caller': 2,
/* 禁止调用console对象的方法 */
'no-console': 'off',
/* 标记修改类声明的变量 */
'no-class-assign': 2,
/* 不允许在试验条件不明确赋值运算符if,for,while,和do...while语句 */
'no-cond-assign': 2,
/* 标记修改使用const关键字声明的变量 */
'no-const-assign': 2,
/* 不允许正则表达式中的控制字符 */
'no-control-regex': 0,
/* 不允许在变量上使用delete操作符 */
'no-delete-var': 2,
/* 不允许在函数声明或表达式中使用重复的参数名称 */
'no-dupe-args': 2,
/* 标记在级别成员中使用重复名称 */
'no-dupe-class-members': 2,
/* 不允许在对象文字中使用重复键 */
'no-dupe-keys': 2,
/* 不允许在switch语句的case子句中使用重复的测试表达式 */
'no-duplicate-case': 2,
/* 不允许在正则表达式中使用空字符类 */
'no-empty-character-class': 2,
/* 不允许空的解构模式 */
'no-empty-pattern': 2,
/* 禁止使用eval()函数来防止潜在的危险 */
'no-eval': 2,
/* 禁止对 catch 子句中的异常重新赋值 */
'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,
/* 不允许重新分配function声明 */
'no-func-assign': 2,
/* 消除隐含eval()通过使用setTimeout(),setInterval()或execScript() */
'no-implied-eval': 2,
/* 不允许function嵌套块中的声明 */
'no-inner-declarations': [2, 'functions'],
/* 不允许RegExp构造函数中的无效正则表达式字符串 */
'no-invalid-regexp': 2,
/* 捕获不是正常制表符和空格的无效空格 */
'no-irregular-whitespace': 2,
/* 防止因使用 __iterator__属性而出现的错误 */
'no-iterator': 2,
/* 禁用与变量同名的标签 */
'no-label-var': 2,
/* 禁用标签语句 */
'no-labels': [2, {
'allowLoop': false,
'allowSwitch': false
}],
/* 禁用不必要的嵌套块 */
'no-lone-blocks': 2,
/* 禁止混用tab和space */
'no-mixed-spaces-and-tabs': 2,
/* 禁止出现多个空格 */
'no-multi-spaces': 2,
/* 禁止多行字符串 */
'no-multi-str': 2,
/* 强制最大连续空行数1 */
'no-multiple-empty-lines': [2, {
'max': 1
}],
/* 不允许修改只读全局变量 */
'no-global-assign': 2,
/* 不允许使用Object构造函数 */
'no-new-object': 2,
/* 消除new require表达的使用 */
'no-new-require': 2,
/* 防止Symbol与new操作员的意外呼叫 */
'no-new-symbol': 2,
/* 杜绝使用String,Number以及Boolean与new操作 */
'no-new-wrappers': 2,
/* 不允许调用Math,JSON和Reflect对象作为功能 */
'no-obj-calls': 2,
/* 不允许使用八进制文字 */
'no-octal': 2,
/* 不允许字符串文字中的八进制转义序列 */
'no-octal-escape': 2,
/* 防止 Node.js 中的目录路径字符串连接 */
'no-path-concat': 2,
/* 当一个对象被__proto__创建时被设置为该对象的构造函数的原始原型属性。getPrototypeOf是获得“原型”的首选方法 */
'no-proto': 2,
/* 消除在同一范围内具有多个声明的变量 */
'no-redeclare': 2,
/* 在正则表达式文字中不允许有多个空格 */
'no-regex-spaces': 2,
/* return陈述中的任务,除非用圆括号括起来,否则不允许赋值 */
'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,
/* 在构造函数中禁止在调用super()之前使用this或super。 */
'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
}],
/* 禁止在 return、throw、continue 和 break 语句后出现不可达代码 */
'no-unreachable': 2,
/* 禁止在 finally 语句块中出现控制流语句 */
'no-unsafe-finally': 2,
/* 禁止未使用过的变量 */
'no-unused-vars': [2, {
'vars': 'all',
'args': 'none'
}],
/* 禁用不必要的 .call() 和 .apply() */
'no-useless-call': 2,
/* 禁止在对象中使用不必要的计算属性 */
'no-useless-computed-key': 2,
/* 禁用不必要的构造函数 */
'no-useless-constructor': 2,
/* 在不改变代码行为的情况下可以安全移除的转义 */
'no-useless-escape': 0,
/* 禁止属性前有空白 */
'no-whitespace-before-property': 2,
/* 禁用 with 语句 */
'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
}],
/* 要求或禁止使用分号代替 ASI */
'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, {
/* 适用于单词类一元操作符,例如:new、delete、typeof、void、yield */
'words': true,
/* 适用于这些一元操作符: -、+、--、++、!、!! */
'nonwords': false
}],
/* 要求或禁止在注释前有空白 */
'spaced-comment': [2, 'always', {
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}],
/* 强制模板字符串中空格的使用 禁止花括号内出现空格 */
'template-curly-spacing': [2, 'never'],
/* 要求调用 isNaN()检查 NaN */
'use-isnan': 2,
/* 强制 typeof 表达式与有效的字符串进行比较 */
'valid-typeof': 2,
/* 需要把立即执行的函数包裹起来 */
'wrap-iife': [2, 'any'],
/* 强制在 yield* 表达式中 * 周围使用空格 */
'yield-star-spacing': [2, 'both'],
/* 要求或者禁止Yoda条件 */
'yoda': [2, 'never'],
/* 建议使用const */
'prefer-const': 2,
/* 禁用 debugger */
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
/* 强制在花括号中使用一致的空格 */
'object-curly-spacing': [2, 'always', {
/* 禁止以对象元素开始或结尾的对象的花括号中有空格 */
objectsInObjects: false
}],
/* 禁止或强制在括号内使用空格 */
'array-bracket-spacing': [2, 'never']
}
}


+ 5
- 0
.gitignore Datei anzeigen

@@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

+ 3
- 0
.vscode/extensions.json Datei anzeigen

@@ -0,0 +1,3 @@
{
"recommendations": ["johnsoncodehk.volar"]
}

+ 7
- 0
README.md Datei anzeigen

@@ -0,0 +1,7 @@
# Vue 3 + Vite

This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.

## Recommended IDE Setup

- [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar)

+ 3
- 0
build/constant.js Datei anzeigen

@@ -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'

+ 14
- 0
build/script/build-cname.js Datei anzeigen

@@ -0,0 +1,14 @@
import chalk from 'chalk'
import { writeFileSync } from 'fs-extra'
import { OUTPUT_DIR } from '../constant'
import { getEnvConfig, getRootPath } from '../utils'

export function runBuildCNAME() {
const { VITE_APP_GLOB_CNAME } = getEnvConfig()
if (!VITE_APP_GLOB_CNAME) return
try {
writeFileSync(getRootPath(`${OUTPUT_DIR}/CNAME`), VITE_APP_GLOB_CNAME)
} catch (error) {
console.log(chalk.red('CNAME file failed to package:\n' + error))
}
}

+ 29
- 0
build/script/build-config.js Datei anzeigen

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

+ 16
- 0
build/script/index.js Datei anzeigen

@@ -0,0 +1,16 @@
import chalk from 'chalk'
import { runBuildConfig } from './build-config'
import { runBuildCNAME } from './build-cname'

export const runBuild = async () => {
try {
runBuildConfig()
runBuildCNAME()
console.log(`✨ ${chalk.cyan('build successfully!')}`)
} catch (error) {
console.log(chalk.red('vite build error:\n' + error))
process.exit(1)
}
}

runBuild()

+ 71
- 0
build/utils.js Datei anzeigen

@@ -0,0 +1,71 @@
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.local', `.env.${mode}`]
}
return ['.env', '.env.local', '.env.production']
}

export function getEnvConfig(match = 'VITE_APP_GLOB_', confFiles = getConfFiles()) {
let envConfig = {}
confFiles.forEach((item) => {
try {
if (fs.existsSync(path.resolve(process.cwd(), item))) {
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)
}

+ 31
- 0
build/vite/plugin/html.js Datei anzeigen

@@ -0,0 +1,31 @@
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
}

+ 26
- 0
build/vite/plugin/index.js Datei anzeigen

@@ -0,0 +1,26 @@
import vue from '@vitejs/plugin-vue'

import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'

import VueSetupExtend from 'vite-plugin-vue-setup-extend'

import { unocss } from './unocss'
import { configHtmlPlugin } from './html'
import { configMockPlugin } from './mock'

export function createVitePlugins(viteEnv, isBuild) {
const plugins = [
vue(),
Components({
resolvers: [NaiveUiResolver()]
}),
VueSetupExtend(),
unocss(),
configHtmlPlugin(viteEnv, isBuild)
]

viteEnv?.VITE_APP_USE_MOCK && plugins.push(configMockPlugin(isBuild))

return plugins
}

+ 14
- 0
build/vite/plugin/mock.js Datei anzeigen

@@ -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/_create-prod-server';
setupProdMockServer();
`
})
}

+ 9
- 0
build/vite/plugin/unocss.js Datei anzeigen

@@ -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()],
})
}

+ 17
- 0
build/vite/proxy.js Datei anzeigen

@@ -0,0 +1,17 @@
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
}

+ 17
- 0
index.html Datei anzeigen

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<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
- 0
jsconfig.json Datei anzeigen

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

+ 14
- 0
mock/_create-prod-server.js Datei anzeigen

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

+ 10
- 0
mock/_util.js Datei anzeigen

@@ -0,0 +1,10 @@
import Mock from 'mockjs'

export function resultSuccess(data, { message = 'ok' } = {}) {
return Mock.mock({
code: 0,
data,
message,
type: 'success'
})
}

+ 17
- 0
mock/login/index.js Datei anzeigen

@@ -0,0 +1,17 @@
import Mock from 'mockjs'
import { resultSuccess } from '../_util'

const Random = Mock.Random

const access_token = Random.string('upper', 32, 32)

export default [
{
url: '/api-mock/login/login',
timeout: 1000,
method: 'post',
response: () => {
return resultSuccess({ access_token })
}
}
]

+ 88
- 0
mock/system/index.js Datei anzeigen

@@ -0,0 +1,88 @@
import Mock from 'mockjs'
import { resultSuccess } from '../_util'
import { asyncRoutes } from './router.js'

const routes = deepClone([...asyncRoutes])

function deepClone(source) {
if (!source && typeof source !== 'object') {
throw new Error('error arguments', 'deepClone')
}
const targetObj = source.constructor === Array ? [] : {}
Object.keys(source).forEach(keys => {
if (source[keys] && typeof source[keys] === 'object') {
targetObj[keys] = deepClone(source[keys])
} else {
targetObj[keys] = source[keys]
}
})
return targetObj
}

const menuList = []
const userList = []
const count = 100

for (let i = 0; i < count; i++) {
menuList.push(Mock.mock({
title: '@name',
'type|1': ['0', '1'],
'method|1': ['put', 'post'],
path: '@name',
component: '@name',
permission: '@name',
'status|1': ['1', '2'],
sort: '@natural',
'hide|1': ['0', '1'],
createTime: '@datetime'
}))
userList.push(Mock.mock({
code: '',
avatar: '@image',
username: '@name',
realname: '@cname',
roles: '',
type: '',
status: '',
deptName: '',
createTime: '@datetime',
updateTime: '@datetime'
}))
}

export default [
{
url: '/api-mock/index/getMenuList',
timeout: 1000,
method: 'get',
response: () => {
return resultSuccess(routes)
}
},
{
url: '/api-mock/menu/index',
timeout: 1000,
method: 'get',
response: config => {
const { page = 1, limit = 10 } = config.query
const mockList = menuList.filter(item => {
return true
})
const List = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1))
return resultSuccess(List)
}
},
{
url: '/api-mock/user/apiIndex',
timeout: 1000,
method: 'get',
response: config => {
const { page = 1, limit = 10 } = config.query
const mockList = userList.filter(item => {
return true
})
const List = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1))
return resultSuccess(List)
}
}
]

+ 41
- 0
mock/system/router.js Datei anzeigen

@@ -0,0 +1,41 @@
const asyncRoutes = [
{
path: '/system',
component: 'Layout',
redirect: '/system/menu',
name: 'System',
meta: {
title: '系统管理'
},
children: [
{
path: 'menu',
component: 'views/system/menu/index',
name: 'SystemMenu',
meta: {
title: '菜单管理'
}
},
{
path: 'user',
component: 'views/system/user/index',
name: 'SystemUser',
meta: {
title: '菜单管理'
}
},
{
path: 'role',
component: 'views/system/role/index',
name: 'SystemRole',
meta: {
title: '角色管理'
}
}
]
}
]

module.exports = {
asyncRoutes
}

+ 17822
- 0
package-lock.json
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 44
- 0
package.json Datei anzeigen

@@ -0,0 +1,44 @@
{
"name": "vite_vue3",
"version": "0.0.0",
"scripts": {
"dev": "vite --mode localhost",
"build:test": "vite build --mode test && esno ./build/script",
"build:dev": "vite build --mode development && esno ./build/script",
"build:prod": "vite build --mode production && esno ./build/script",
"serve": "vite preview"
},
"dependencies": {
"@vicons/antd": "^0.10.0",
"@vicons/ionicons5": "^0.10.0",
"axios": "^0.26.1",
"mockjs": "^1.1.0",
"pinia": "^2.0.13",
"vue": "^3.2.16",
"vue-router": "^4.0.14",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@unocss/preset-attributify": "^0.16.4",
"@unocss/preset-icons": "^0.16.4",
"@unocss/preset-uno": "^0.16.4",
"@vitejs/plugin-vue": "^1.9.3",
"@vue/cli-plugin-eslint": "^5.0.4",
"babel-eslint": "^10.1.0",
"chalk": "^5.0.1",
"dotenv": "^10.0.0",
"eslint": "^7.19.0",
"eslint-plugin-html": "^6.2.0",
"eslint-plugin-vue": "^8.5.0",
"esno": "^0.13.0",
"fs-extra": "^10.0.1",
"naive-ui": "^2.27.0",
"sass": "^1.49.11",
"unocss": "^0.16.4",
"unplugin-vue-components": "^0.18.5",
"vite": "^2.6.4",
"vite-plugin-html": "^2.1.2",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-vue-setup-extend": "^0.4.0"
}
}

BIN
public/favicon.ico Datei anzeigen

Vorher Nachher

+ 24
- 0
src/App.vue Datei anzeigen

@@ -0,0 +1,24 @@
<template>
<n-config-provider inline-theme-disabled :theme-overrides="themeOverrides">
<n-loading-bar-provider>
<loading-bar />
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
</n-loading-bar-provider>
</n-config-provider>
</template>

<script setup>
import themeOverrides from '@/utils/ui/theme.js'
import LoadingBar from '@/components/LoadingBar/index.vue'
</script>

<style lang="scss">
#app {
height: 100%;
.n-config-provider {
height: inherit;
}
}
</style>

+ 16
- 0
src/api/auth/index.js Datei anzeigen

@@ -0,0 +1,16 @@
import { defAxios as request } from '@/utils/http'

export const login = (data) => {
return request({
url: '/auth/login',
method: 'post',
data,
})
}

export const refreshToken = () => {
return request({
url: '/auth/refreshToken',
method: 'post',
})
}

+ 15
- 0
src/api/login/index.js Datei anzeigen

@@ -0,0 +1,15 @@
import { mockAxios as request } from '@/utils/http'

export function userLogin(data = {}) {
return request({
url: '/login/login',
method: 'post',
data
})
}
export function userCaptcha() {
return request({
url: '/login/captcha',
method: 'get'
})
}

+ 39
- 0
src/api/post/index.js Datei anzeigen

@@ -0,0 +1,39 @@
import { defAxios as request } from '@/utils/http'

export function getPosts(data = {}) {
return request({
url: '/posts',
method: 'get',
data,
})
}

export function getPostById({ id }) {
return request({
url: `/post/${id}`,
method: 'get',
})
}

export function savePost(id, data = {}) {
if (id) {
return request({
url: `/post/${id}`,
method: 'put',
data,
})
}

return request({
url: '/post',
method: 'post',
data,
})
}

export function deletePost(id) {
return request({
url: `/post/${id}`,
method: 'delete',
})
}

+ 23
- 0
src/api/system/index.js Datei anzeigen

@@ -0,0 +1,23 @@
import { mockAxios as request } from '@/utils/http'

export function getMenu() {
return request({
url: '/index/getMenuList',
method: 'GET'
})
}

export function getMenuList() {
return request({
url: '/menu/index',
method: 'GET'
})
}

export function getUserList(params) {
return request({
url: '/user/apiIndex',
method: 'GET',
params
})
}

+ 38
- 0
src/api/user/index.js Datei anzeigen

@@ -0,0 +1,38 @@
import { defAxios as request } from '@/utils/http'

export function getUsers(data = {}) {
return request({
url: '/users',
method: 'get',
data,
})
}

export function getUser(id) {
if (id) {
return request({
url: `/user/${id}`,
method: 'get',
})
}
return request({
url: '/user',
method: 'get',
})
}

export function saveUser(data = {}, id) {
if (id) {
return request({
url: '/user',
method: 'put',
data,
})
}

return request({
url: `/user/${id}`,
method: 'put',
data,
})
}

BIN
src/assets/logo.png Datei anzeigen

Vorher Nachher
Breite: 200  |  Höhe: 200  |  Größe: 6.7KB

+ 24
- 0
src/components/AppProvider/index.vue Datei anzeigen

@@ -0,0 +1,24 @@
<template>
<n-config-provider inline-theme-disabled :theme-overrides="themeOverrides">
<n-loading-bar-provider>
<!-- <loading-bar /> -->
<n-dialog-provider>
<dialog-content />
<n-message-provider>
<message-content />
<slot />
</n-message-provider>
</n-dialog-provider>
</n-loading-bar-provider>
</n-config-provider>
</template>

<script setup>
import themeOverrides from '@/utils/ui/theme.js'
// 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>

+ 22
- 0
src/components/Bread/index.vue Datei anzeigen

@@ -0,0 +1,22 @@
<template>
<n-breadcrumb>
<n-breadcrumb-item href="#">
北京总行
</n-breadcrumb-item>
<n-breadcrumb-item href="#">
天津分行
</n-breadcrumb-item>
<n-breadcrumb-item href="#">
平山道支行
</n-breadcrumb-item>
</n-breadcrumb>
</template>

<script>
import { defineComponent } from 'vue'

export default defineComponent({
components: {
}
})
</script>

+ 72
- 0
src/components/DataTable/index.vue Datei anzeigen

@@ -0,0 +1,72 @@
<template>
<div class="table-toolbar">
<!--顶部左侧区域-->
<div class="table-toolbar-left">
<slot name="tableTitle" />
</div>

<div class="table-toolbar-right">
<!--顶部右侧区域-->
<slot name="toolbar" />
<!--刷新-->
<span @click="reload">刷新</span>
</div>
</div>
<div class="s-table">
<n-data-table v-bind="getBindProps" />
</div>
</template>

<script>
import { NDataTable } from 'naive-ui'
import { unref, computed } from 'vue'
export default {
name: 'DataTable',
props: {
...NDataTable.props
},
setup(props, { emit }) {
const getBindProps = computed(() => {
return {
...unref(props)
}
})
console.log(getBindProps)
return {
getBindProps
}
}
}

</script>
<style scoped lang='scss'>
.table-toolbar {
display: flex;
justify-content: space-between;
padding: 15px 10px;
.table-toolbar-left {
display: flex;
align-items: center;
justify-content: flex-start;
flex: 1;
}
.table-toolbar-right {
display: flex;
justify-content: flex-end;
flex: 1;
.table-toolbar-icon {
margin-left: 12px;
font-size: 16px;
cursor: pointer;
color: var(--text-color);
&:hover {
color: #1890ff;
}
}
}
}
.table-toolbar-inner-popover-title {
padding: 2px 0;
}

</style>

+ 71
- 0
src/components/DataTable/tools/Action.vue Datei anzeigen

@@ -0,0 +1,71 @@
<template>
<div class="tableAction" :style="`justify-content: ${getAlign}`">
<template v-for="(action, index) in getActions" :key="`${index}-${action.label}`">
<n-button v-if="!action.type || action.type === 'button'" class="tableAction__item" v-bind="action.props">{{ action.label }}</n-button>
<n-popconfirm
v-if="action.type === 'popconfirm'"
v-bind="action.props"
>
<template #trigger>
<n-button v-bind="action.ButtonProps" class="tableAction__item">{{ action.label }}</n-button>
</template>
{{ action.tip }}
</n-popconfirm>
</template>
</div>
</template>

<script>
import { defineComponent, computed, toRaw, reactive } from 'vue'
export default defineComponent({
name: 'TableAction',
props: {
actions: {
type: Array,
default: null,
required: true
},
align: {
type: String,
default: 'center',
validator: (value) => {
return ['left', 'right', 'center'].indexOf(value) !== -1
}
}
},
setup(props, { emit }) {
const data = reactive({
permissionList: [
'basic_list'
]
})

const getActions = computed(() => {
return (toRaw(props.actions) || [])
.filter((action) => {
return data.permissionList.includes(action.auth) || action.auth === ''
})
})

const getAlign = computed(() => {
return toRaw(props.align)
})

return {
getActions,
getAlign
}
}
})

</script>
<style scoped lang='scss'>
.tableAction{
display: flex;
align-items: center;
.tableAction__item{
margin: 0 5px;
}
// justify-content: center;
}
</style>

+ 30
- 0
src/components/DataTable/tools/Image.vue Datei anzeigen

@@ -0,0 +1,30 @@
<template>
<n-image v-bind="getProps" />
</template>

<script>
import { defineComponent, computed, toRaw } from 'vue'
export default defineComponent({
name: 'TableImage',
props: {
images: {
type: Object,
default: null,
required: true
}
},
setup(props, { emit }) {
const getProps = computed(() => {
return (toRaw(props.images))
})

return {
getProps
}
}
})

</script>

<style scoped lang='scss'>
</style>

+ 134
- 0
src/components/DataTable_back/index.vue Datei anzeigen

@@ -0,0 +1,134 @@
<template>
<!-- <n-data-table>
n-data-table
</n-data-table> -->
<div class="table-toolbar">
<!--顶部左侧区域-->
<div class="flex items-center table-toolbar-left">
<slot name="tableTitle" />
</div>

<div class="flex items-center table-toolbar-right">
<!--顶部右侧区域-->
<slot name="toolbar" />
<!--刷新-->
<!-- <n-tooltip trigger="hover">
<template #trigger>
<div class="table-toolbar-right-icon" @click="reload">
<n-icon size="18">
<ReloadOutlined />
</n-icon>
</div>
</template> -->
<span @click="reload">刷新</span>
<!-- </n-tooltip> -->
<!--表格设置单独抽离成组件-->
<!-- <ColumnSetting /> -->
</div>
</div>
<div class="s-table">
<n-data-table
ref="tableElRef"
v-bind="getBindValues"
:pagination="pagination"
>
<template v-for="item in Object.keys($slots)" #[item]="data" :key="item">
<slot :name="item" v-bind="data" />
</template>
</n-data-table>
</div>
</template>

<script>
import { tableProps } from './tools/props.js'
import { useDataSource } from './tools/useDataSource.js'
import { usePagination } from './tools/usePagination.js'

import { unref, ref, computed, toRaw } from 'vue'
export default {
name: 'DataTable',
props: {
...tableProps
},
emits: [
'fetch-success',
'fetch-error',
'update:checked-row-keys',
'edit-end',
'edit-cancel',
'edit-row-end',
'edit-change'
],
setup(props, { emit }) {
const loadingRef = ref(unref(props).loading)
const getLoading = computed(() => unref(loadingRef))
function setLoading(loading) {
loadingRef.value = loading
}

/* pagination-start */
const pagination = computed(() => toRaw(unref(getPaginationInfo)))
const { getPaginationInfo, setPagination } = usePagination(props)
/* pagination-end */

/* tableData-start */
const tableData = ref([])
const { getDataSourceRef, reload } = useDataSource(props, { getPaginationInfo, setPagination, tableData, setLoading }, emit)

// 组装表格信息
const getBindValues = computed(() => {
const tableData = unref(getDataSourceRef)
return {
...unref(props),
loading: unref(getLoading),
// columns: toRaw(unref(getPageColumns)),
// rowKey: unref(getRowKey),
data: tableData,
// size: unref(getTableSize),
remote: true,
'max-height': 'auto'
}
})
/* tableData-end */

return {
pagination,
fetch,
reload,
getBindValues
}
}
}

</script>
<style scoped lang='scss'>
.table-toolbar {
display: flex;
justify-content: space-between;
padding: 0 0 16px 0;
.table-toolbar-left {
display: flex;
align-items: center;
justify-content: flex-start;
flex: 1;
}
.table-toolbar-right {
display: flex;
justify-content: flex-end;
flex: 1;
.table-toolbar-icon {
margin-left: 12px;
font-size: 16px;
cursor: pointer;
color: var(--text-color);
&:hover {
color: #1890ff;
}
}
}
}
.table-toolbar-inner-popover-title {
padding: 2px 0;
}

</style>

+ 35
- 0
src/components/DataTable_back/tools/props.js Datei anzeigen

@@ -0,0 +1,35 @@
import { NDataTable } from 'naive-ui'

export const tableProps = {
...NDataTable.props,
/* 初始化接口请求 */
request: {
type: Function,
default: null
},
/* 分页信息 */
pagination: {
type: [Object, Boolean],
default: () => {}
},
/* 分页设置信息 */
paginationSetting: {
type: Object,
default: () => {
return {
// 当前页的字段名
pageField: 'page',
// 每页数量字段名
sizeField: 'pageSize',
// 接口返回的数据字段名
listField: 'list',
// 接口返回总页数字段名
totalField: 'pageCount',
// 默认分页数量
defaultPageSize: 10,
// 可切换每页数量集合
pageSizes: [10, 20, 30, 40, 50]
}
}
}
}

+ 99
- 0
src/components/DataTable_back/tools/useDataSource.js Datei anzeigen

@@ -0,0 +1,99 @@
import { ref, unref, computed, onMounted } from 'vue'
import { isBoolean } from '@/utils/is'

export function useDataSource(propsRef, { getPaginationInfo, setPagination, setLoading, tableData }, emit) {
const dataSourceRef = ref([])

async function fetch(opt) {
try {
// setLoading(true)
const { request, pagination } = unref(propsRef)
/* 无接口请求中断 */
if (!request) return
/* 获取分页信息 */
const paginationSetting = propsRef.paginationSetting
const pageField = paginationSetting.pageField
const sizeField = paginationSetting.sizeField
const totalField = paginationSetting.totalField
const listField = paginationSetting.listField

let pageParams = {}
const { page = 1, pageSize = 10 } = unref(getPaginationInfo)
if ((isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)) {
pageParams = {}
} else {
pageParams[pageField] = (opt && opt[pageField]) || page
pageParams[sizeField] = pageSize
}

const params = {
...pageParams
}
const res = await request(params)
console.log('res', res)
const resultTotal = res[totalField] || 0
const currentPage = res[pageField]

// // 如果数据异常,需获取正确的页码再次执行
// if (resultTotal) {
// if (page > resultTotal) {
// setPagination({
// [pageField]: resultTotal
// })
// fetch(opt)
// }
// }
const resultInfo = res[listField] ? res[listField] : []
dataSourceRef.value = resultInfo
setPagination({
[pageField]: currentPage,
[totalField]: resultTotal
})
// if (opt && opt[pageField]) {
// setPagination({
// [pageField]: opt[pageField] || 1
// })
// }
emit('fetch-success', {
items: unref(resultInfo),
resultTotal
})
} catch (error) {
console.error(error)
// emit('fetch-error', error)
// dataSourceRef.value = []
} finally {
setLoading(false)
}
}

const getDataSourceRef = computed(() => {
const dataSource = unref(dataSourceRef)
if (!dataSource || dataSource.length === 0) {
return unref(dataSourceRef)
}
return unref(dataSourceRef)
})

function getDataSource() {
console.log(getDataSourceRef.value)
return getDataSourceRef.value
}

function setTableData(values) {
dataSourceRef.value = values
}

onMounted(() => {
setTimeout(() => {
fetch()
}, 15)
})

return {
fetch,
getDataSourceRef,
getDataSource,
setTableData
}
}

+ 34
- 0
src/components/DataTable_back/tools/usePagination.js Datei anzeigen

@@ -0,0 +1,34 @@
import { computed, unref, ref } from 'vue'
import { isBoolean } from '@/utils/is'

export function usePagination(refProps) {
const configRef = ref({})
const show = ref(true)
console.log('configRef', configRef)
console.log('refProps', refProps)
const getPaginationInfo = computed(() => {
const { pagination, paginationSetting } = unref(refProps)
if (!unref(show) || (isBoolean(pagination) && !pagination)) {
return false
}
return {
pageSize: paginationSetting.defaultPageSize,
pageSizes: paginationSetting.pageSizes,
showSizePicker: true,
showQuickJumper: true,
...(isBoolean(pagination) ? {} : pagination),
...unref(configRef),
pageCount: unref(configRef)[paginationSetting.totalField]
}
})

function setPagination(info) {
const paginationInfo = unref(getPaginationInfo)
configRef.value = {
...(!isBoolean(paginationInfo) ? paginationInfo : {}),
...info
}
}

return { getPaginationInfo, setPagination }
}

+ 52
- 0
src/components/Dialog/index.vue Datei anzeigen

@@ -0,0 +1,52 @@
<template />

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

+ 17
- 0
src/components/LoadingBar/index.vue Datei anzeigen

@@ -0,0 +1,17 @@
<template>
<div />
</template>
<script>
export default {
name: 'LoadingBar'
}
</script>
<script setup>
import { useLoadingBar } from 'naive-ui'
window['$loadingBar'] = useLoadingBar()
Object.defineProperty(window, '$loadingBar', {
configurable: false,
writable: false
})
</script>


+ 72
- 0
src/components/Message/index.vue Datei anzeigen

@@ -0,0 +1,72 @@
<template />

<script setup>
import { useMessage } from 'naive-ui'

const NMessage = useMessage()

let loadingMessage = null

class Message {
/**
* 规则:
* * loading message只显示一个,新的message会替换正在显示的loading message
* * loading message不会自动清除,除非被替换成非loading message,非loading message默认2秒后自动清除
*/

removeMessage(message, duration = 2000) {
setTimeout(() => {
if (message) {
message.destroy()
message = null
}
}, duration)
}

showMessage(type, content, option = {}) {
if (loadingMessage && loadingMessage.type === 'loading') {
// 如果存在则替换正在显示的loading message
loadingMessage.type = type
loadingMessage.content = content

if (type !== 'loading') {
// 非loading message需设置自动清除
this.removeMessage(loadingMessage, option.duration)
}
} else {
// 不存在正在显示的loading则新建一个message,如果新建的message是loading message则将message赋值存储下来
const message = NMessage[type](content, option)
if (type === 'loading') {
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>

+ 15
- 0
src/layout/components/Header/index.vue Datei anzeigen

@@ -0,0 +1,15 @@
<template>
<n-layout-header class="layout__header" bordered>
Header Header Header
<!-- <SideMenu menu-mode="horizontal" /> -->
</n-layout-header>
</template>

<script>
import SideMenu from '@/layout/components/Menu/index.vue'
import { defineComponent } from 'vue'
export default defineComponent({
name: 'LayoutHeader',
components: { SideMenu }
})
</script>

+ 71
- 0
src/layout/components/Menu/index.vue Datei anzeigen

@@ -0,0 +1,71 @@
<template>
<n-menu
:mode="menuMode"
:value="(currentRoute.meta && currentRoute.meta.activeMenu) || currentRoute.name"
:options="getMenuOptions"
@update:value="handleMenuSelect"
/>
</template>

<script setup>
import { useRouter } from 'vue-router'
import { computed, defineProps, toRaw } from 'vue'
import { isExternal } from '@/utils/is.js'
import { usePermissionStore } from '@/store/modules/permission'

const props = defineProps({
menuMode: {
type: String,
default: 'vertical'
}
})

const menuMode = toRaw(props.menuMode)

const router = useRouter()
const { currentRoute } = router
const permissionStore = usePermissionStore()
const getMenuOptions = computed(() => {
return generateOptions(permissionStore.routes, '')
})

function resolvePath(basePath, path) {
if (isExternal(path)) return path
return (
'/' +
[basePath, path]
.filter((path) => !!path && path !== '/')
.map((path) => path.replace(/(^\/)|(\/$)/g, ''))
.join('/')
)
}

function generateOptions(routes, basePath) {
const options = []
routes.forEach((route) => {
if (route.name && !route.isHidden) {
const 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))
}
options.push(curOption)
}
})
return options
}

function handleMenuSelect(key, item) {
if (isExternal(item.path)) {
window.open(item.path)
} else {
router.push(item.path)
}
}

</script>
<style scoped lang='scss'>
</style>

+ 23
- 0
src/layout/components/Sidebar/index.vue Datei anzeigen

@@ -0,0 +1,23 @@
<template>
<n-layout-sider
class="layout_sidebar"
v-bind="getSideOptions"
>
<div class="project__logo">
11
</div>
<SideMenu />
</n-layout-sider>
</template>

<script setup>
import { computed, toRaw } from 'vue'
import SideMenu from '@/layout/components/Menu/index.vue'
import { useSettingStore } from '@/store/modules/setting.js'
const settingStore = useSettingStore()

const getSideOptions = computed(() => {
return toRaw(settingStore.getSidebarSetting)
})

</script>

+ 250
- 0
src/layout/components/Tags/index.vue Datei anzeigen

@@ -0,0 +1,250 @@
<template>
<div class="tabs-view" :class="{'tabs-view-fix': tagsMenuSetting.fixed,}">
<div class="tabs-view-main">
<div ref="navWrap" class="tabs-card" :class="{ 'tabs-card-scrollable': scrollable }">
<span class="tabs-card-prev" :class="{ 'tabs-card-prev-hide': !scrollable }" @click="scrollPrev">
<n-icon size="16" color="#515a6e">
<LeftOutlined />
</n-icon>
</span>
<span class="tabs-card-next" :class="{ 'tabs-card-next-hide': !scrollable }" @click="scrollNext">
<n-icon size="16" color="#515a6e">
<RightOutlined />
</n-icon>
</span>
<div ref="navScroll" class="tabs-card-scroll">
<Draggable :list="tabsList" animation="300" item-key="fullPath" class="flex">
<template #item="{ element }">
<div
:id="`tag${element.fullPath.split('/').join('\/')}`"
class="tabs-card-scroll-item"
:class="{ 'active-item': activeKey === element.path }"
@click.stop="goPage(element)"
@contextmenu="handleContextMenu($event, element)"
>
<span>{{ element.meta.title }}</span>
<n-icon v-if="!element.meta.affix" size="14" @click.stop="closeTabItem(element)">
<CloseOutlined />
</n-icon>
</div>
</template>
</Draggable>
</div>
</div>
</div>
</div>
</template>

<script setup>
import {
LeftOutlined,
RightOutlined,
CloseOutlined
} from '@vicons/antd'
import Draggable from 'vuedraggable'
import { reactive, computed, toRaw } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTagsMenuStore } from '@/store/modules/tagsMenu.js'
import { useSettingStore } from '@/store/modules/setting.js'
import { getTags, setTags } from '@/utils/tags.js'

/* 获取路由器 */
const route = useRoute()
const router = useRouter()
const tagsMenuStore = useTagsMenuStore()
const settingStore = useSettingStore()
const tabsList = computed(() => tagsMenuStore.tabsList)
const tagsMenuSetting = computed(() => settingStore.getTagsMenuSetting)

let cacheRoutes = []
const simpleRoute = getSimpleRoute(route)
try {
const routesStr = getTags()
cacheRoutes = routesStr ? JSON.parse(routesStr) : [simpleRoute]
} catch (e) {
cacheRoutes = [simpleRoute]
}

/* 同步路由信息 */
const routes = router.getRoutes()
cacheRoutes.forEach((cacheRoute) => {
const findRoute = routes.find((route) => route.path === cacheRoute.path)
if (route) {
cacheRoute.meta = findRoute.meta || cacheRoute.meta
cacheRoute.name = (findRoute.name || cacheRoute.name)
}
})

tagsMenuStore.initTabs(cacheRoutes)

const state = reactive({
activeKey: route.fullPath,
scrollable: false
})

/**
* @description:
* @param { Object } route
* @return { Object }
*/
function getSimpleRoute(route) {
const { fullPath, hash, meta, name, params, path, query } = route
return { fullPath, hash, meta, name, params, path, query }
}

/**
* @description: 左侧滚动
* @return {*}
*/
function scrollPrev() {
// const containerWidth = navScroll.value.offsetWidth
// const currentScroll = navScroll.value.scrollLeft
// if (!currentScroll) return
// const scrollLeft = currentScroll > containerWidth ? currentScroll - containerWidth : 0
// scrollTo(scrollLeft, (scrollLeft - currentScroll) / 20)
}
/**
* @description: 右侧滚动
* @return {*}
*/
function scrollNext() {
// const containerWidth = navScroll.value.offsetWidth
// const currentScroll = navScroll.value.scrollLeft
// if (!currentScroll) return
// const scrollLeft = currentScroll > containerWidth ? currentScroll - containerWidth : 0
// scrollTo(scrollLeft, (scrollLeft - currentScroll) / 20)
}
/**
* @description: 页面跳转
* @return {*}
*/
function goPage(e) {
// const { fullPath } = e
// if (fullPath === route.fullPath) return
// state.activeKey = fullPath
// go(e, true)
}
/**
* @description: 删除项
* @return {*}
*/
// 删除tab
function closeTabItem(e) {
// const { fullPath } = e
// const routeInfo = tabsList.value.find((item) => item.fullPath == fullPath)
// removeTab(routeInfo)
}
/**
* @description: 右键菜单
* @return {*}
*/
function handleContextMenu(e, item) {
// e.preventDefault()
// isCurrent.value = PageEnum.BASE_HOME_REDIRECT === item.path
// state.showDropdown = false
// nextTick().then(() => {
// state.showDropdown = true
// state.dropdownX = e.clientX
// state.dropdownY = e.clientY
// })
}

</script>
<style scoped lang='scss'>
.tabs-view {
width: 100%;
padding: 6px 0;
display: flex;
transition: all 0.2s ease-in-out;
.tabs-view-main {
height: 32px;
display: flex;
max-width: 100%;
min-width: 100%;
.tabs-card{
-webkit-box-flex: 1;
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
position: relative;
&.tabs-card-scrollable {
padding: 0 32px;
overflow: hidden;
}
.tabs-card-prev,
.tabs-card-next {
width: 32px;
text-align: center;
position: absolute;
line-height: 32px;
cursor: pointer;
.n-icon {
display: flex;
align-items: center;
justify-content: center;
height: 32px;
width: 32px;
}
}
.tabs-card-scroll {
white-space: nowrap;
overflow: hidden;
&-item {
background: v-bind(getCardColor);
color: v-bind(getBaseColor);
height: 32px;
padding: 6px 16px 4px;
border-radius: 3px;
margin-right: 6px;
cursor: pointer;
display: inline-block;
position: relative;
flex: 0 0 auto;

span {
float: left;
vertical-align: middle;
}

&:hover {
color: #515a6e;
}

.n-icon {
height: 22px;
width: 21px;
margin-right: -6px;
position: relative;
vertical-align: middle;
text-align: center;
color: #808695;

&:hover {
color: #515a6e !important;
}

svg {
height: 21px;
display: inline-block;
}
}
}
// .active-item {
// color: v-bind(getAppTheme);
// }
}
}
}
}

.tabs-view-fix {
position: fixed;
z-index: 5;
padding: 6px 19px 6px 10px;
left: 200px;
}

.tabs-view-default-background {
background: #f5f7f9;
}
</style>

+ 43
- 0
src/layout/index.vue Datei anzeigen

@@ -0,0 +1,43 @@
<template>
<!-- <n-space class="layout" :size="0" vertical>
<n-layout>
<Header />
</n-layout>
<n-layout has-sider>
<SideBar />
<n-layout class="layout__content">
<Bread />
<router-view />
</n-layout>
</n-layout>
</n-space> -->
<n-space class="layout" :size="0" vertical>
<n-layout has-sider>
<SideBar v-if="menuMode === 'sidebar'" />
<n-layout>
<Header />
<Tags v-if="tagsMenuSetting.show" />
<n-layout class="layout__content">
<router-view />
</n-layout>
</n-layout>
</n-layout>
</n-space>
</template>

<script setup>
// import { reactive } from 'vue'
import Header from './components/Header/index.vue'
import SideBar from './components/Sidebar/index.vue'
import Tags from './components/Tags/index.vue'
import { useSettingStore } from '@/store/modules/setting.js'
import { computed } from 'vue'
const settingStore = useSettingStore()
const menuMode = computed(() => settingStore.getMenuMode)
const tagsMenuSetting = computed(() => settingStore.getTagsMenuSetting)

</script>

<style lang="scss" scoped>

</style>

+ 19
- 0
src/main.js Datei anzeigen

@@ -0,0 +1,19 @@
import '@/styles/index.scss'
import 'uno.css'

import { createApp } from 'vue'
import { setupRouter } from '@/router'
import { setupStore } from '@/store'

import App from './App.vue'

function setupApp() {
const app = createApp(App)

setupStore(app)
setupRouter(app)

app.mount('#app')
}

setupApp()

+ 9
- 0
src/router/guard/index.js Datei anzeigen

@@ -0,0 +1,9 @@
import { createPageLoadingGuard } from './page-loading-guard'
import { createPageTitleGuard } from './page-title-guard'
import { createPermissionGuard } from './permission-guard'

export function setupRouterGuard(router) {
createPageLoadingGuard(router)
createPermissionGuard(router)
createPageTitleGuard(router)
}

+ 15
- 0
src/router/guard/page-loading-guard.js Datei anzeigen

@@ -0,0 +1,15 @@
export function createPageLoadingGuard(router) {
router.beforeEach(() => {
window.$loadingBar?.start()
})

router.afterEach(() => {
setTimeout(() => {
window.$loadingBar?.finish()
}, 200)
})

router.onError(() => {
window.$loadingBar?.error()
})
}

+ 12
- 0
src/router/guard/page-title-guard.js Datei anzeigen

@@ -0,0 +1,12 @@
const baseTitle = import.meta.env.VITE_APP_TITLE

export function createPageTitleGuard(router) {
router.afterEach((to) => {
const pageTitle = to.meta?.title
if (pageTitle) {
document.title = `${pageTitle} | ${baseTitle}`
} else {
document.title = baseTitle
}
})
}

+ 42
- 0
src/router/guard/permission-guard.js Datei anzeigen

@@ -0,0 +1,42 @@
import { useUserStore } from '@/store/modules/user'
import { usePermissionStore } from '@/store/modules/permission'
import { NOT_FOUND_ROUTE } from '@/router/routes'
import { getToken } 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 {
const hasRoutes = !!permissionStore.permissionRoutes.length
console.log(permissionStore.permissionRoutes)
if (hasRoutes) {
next()
} else {
try {
// await userStore.getUserInfo()
const routes = await permissionStore.generateRoutes()
router.addRoute(routes[0])
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 }})
}
}
})
}

+ 24
- 0
src/router/index.js Datei anzeigen

@@ -0,0 +1,24 @@
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
router.hasRoute(name) && router.removeRoute(name)
})
basicRoutes.forEach((route) => {
!router.hasRoute(route.name) && router.addRoute(route)
})
}

export function setupRouter(app) {
app.use(router)
setupRouterGuard(router)
}

+ 48
- 0
src/router/routes/index.js Datei anzeigen

@@ -0,0 +1,48 @@
import Layout from '@/layout/index.vue'
import Home from '@/views/dashboard/index.vue'

export const basicRoutes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
isHidden: true,
meta: {
title: '登录页'
}
},
{
path: '/',
name: 'Dashboard',
component: Layout,
redirect: '/home',
meta: {
title: 'Dashboard'
},
children: [
{
path: 'home',
name: 'Home',
component: Home,
meta: {
title: '首页'
}
}
]
}
]

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 }

+ 23
- 0
src/router/routes/modules/system.js Datei anzeigen

@@ -0,0 +1,23 @@
import Layout from '@/layout/index.vue'
export default [
{
path: '/system',
component: Layout,
redirect: '/system/menu',
name: 'System',
meta: {
title: '系统管理'
},
children: [
{
path: 'menu',
component: () => import('@/views/system/menu/index.vue'),
name: 'SystemMenu',
meta: {
title: '菜单管理'
}
}
]
}
]


+ 25
- 0
src/setting/config.js Datei anzeigen

@@ -0,0 +1,25 @@
const setting = {
/* 布局模式 vertical / horizontal */
/* layoutMode: 'vertical', */
/* 导航模式 sidebar / header */
menuMode: 'sidebar',
headerSetting: {
isReload: true
},
/* 侧边栏属性 */
sidebarSetting: {
isBorder: true,
isTrigger: false,
mode: 'width',
cWidth: 48,
width: 212,
isScroll: false
},
/* tags */
tagsMenuSetting: {
show: true,
fixed: true,
background: '#f5f7f9'
}
}
export default setting

+ 5
- 0
src/store/index.js Datei anzeigen

@@ -0,0 +1,5 @@
import { createPinia } from 'pinia'

export function setupStore(app) {
app.use(createPinia())
}

+ 17
- 0
src/store/modules/app.js Datei anzeigen

@@ -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'
}
}
}
}
})

+ 111
- 0
src/store/modules/permission.js Datei anzeigen

@@ -0,0 +1,111 @@
import { defineStore } from 'pinia'
import { asyncRoutes, basicRoutes } from '@/router/routes'
import { getMenu } from '@/api/system'
import Layout from '@/layout/index.vue'
import modules from '@/utils/module.js'

/**
* @description:
* @param {*} route
* @param {*} role
* @return {*}
*/
function hasPermission(route, role) {
// const routeRole = route.meta?.role ? route.meta.role : []
// if (!role.length || !routeRole.length) {
// return false
// }
// return role.some((item) => routeRole.includes(item))
return true
}

/**
* @description: 过滤权限路由
* @param {*} routes
* @param {*} role
* @return {*}
*/
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
}

/**
* @description:
* @param {*} routes
* @return {*}
*/
function dataArrayToRoutes(routes) {
const res = []
routes.forEach(item => {
const tmp = { ...item }
// 如果有component配置
if (tmp.component) {
// Layout引入
if (tmp.component === 'Layout') {
tmp.component = Layout
} else {
const sub_view = tmp.component.replace(/^\/*/g, '')
const component = `../${sub_view}.vue`
tmp.component = modules[component]
}
if (tmp.children) {
tmp.children = dataArrayToRoutes(tmp.children)
}
}
res.push(tmp)
})
return res
}

export const usePermissionStore = defineStore('permission', {
state() {
return {
accessRoutes: []
}
},
getters: {
routes() {
return basicRoutes.concat(this.accessRoutes)
},
permissionRoutes() {
return this.accessRoutes
}
},
actions: {
generateRoutesMock(role = []) {
const accessRoutes = filterAsyncRoutes(asyncRoutes, role)
this.accessRoutes = accessRoutes
return accessRoutes
},
async generateRoutes() {
try {
const res = await getMenu()
if (res.code === 0) {
const result = dataArrayToRoutes(res.data)
this.accessRoutes = result
return Promise.resolve(result)
} else {
return Promise.reject(res.message)
}
} catch (error) {
console.error(error)
return Promise.reject(error.message)
}
}
}
})

+ 25
- 0
src/store/modules/setting.js Datei anzeigen

@@ -0,0 +1,25 @@
import { defineStore } from 'pinia'
import config from '@/setting/config.js'

export const useSettingStore = defineStore({
id: 'project-setting',
state: () => ({
...config
}),
getters: {
getMenuMode() {
return this.menuMode
},
getHeaderSetting() {
return this.headerSetting
},
getSidebarSetting() {
return this.sidebarSetting
},
getTagsMenuSetting() {
return this.tagsMenuSetting
}
},
actions: {
}
})

+ 56
- 0
src/store/modules/tagsMenu.js Datei anzeigen

@@ -0,0 +1,56 @@
import { defineStore } from 'pinia'

// 不需要出现在标签页中的路由
const whiteList = ['Redirect', 'login']

// 保留固定路由
function retainAffixRoute(list) {
return list.filter((item) => item?.meta?.affix ?? false)
}

export const useTagsMenuStore = defineStore({
id: 'project-tags-menu',
state: () => ({
tabsList: []
}),
getters: {},
actions: {
initTabs(routes) {
// 初始化标签页
this.tabsList = routes
},
addTabs(route) {
// 添加标签页
if (whiteList.includes(route.name)) return false
const isExists = this.tabsList.some((item) => item.fullPath === route.fullPath)
if (!isExists) {
this.tabsList.push(route)
}
return true
},
closeLeftTabs(route) {
// 关闭左侧
const index = this.tabsList.findIndex((item) => item.fullPath === route.fullPath)
this.tabsList.splice(0, index)
},
closeRightTabs(route) {
// 关闭右侧
const index = this.tabsList.findIndex((item) => item.fullPath === route.fullPath)
this.tabsList.splice(index + 1)
},
closeOtherTabs(route) {
// 关闭其他
this.tabsList = this.tabsList.filter((item) => item.fullPath === route.fullPath)
},
closeCurrentTab(route) {
// 关闭当前页
const index = this.tabsList.findIndex((item) => item.fullPath === route.fullPath)
this.tabsList.splice(index, 1)
},
closeAllTabs() {
// 关闭全部
console.log(retainAffixRoute(this.tabsList))
this.tabsList = retainAffixRoute(this.tabsList)
}
}
})

+ 49
- 0
src/store/modules/user.js Datei anzeigen

@@ -0,0 +1,49 @@
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 = {}
},
setUserInfo(userInfo = {}) {
this.userInfo = { ...this.userInfo, ...userInfo }
}
}
})

+ 3
- 0
src/styles/index.scss Datei anzeigen

@@ -0,0 +1,3 @@
@import './reset.scss';
@import './public.scss';
@import './layout.scss';

+ 12
- 0
src/styles/layout.scss Datei anzeigen

@@ -0,0 +1,12 @@
.layout,.n-space{
height: inherit;
}

.layout{
.layout__header{
height: 48px;
}
.layout__content{
height: calc(100vh - 48px);
}
}

+ 56
- 0
src/styles/public.scss Datei anzeigen

@@ -0,0 +1,56 @@
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);
}

/* 滚动条凹槽的颜色,还可以设置边框属性 */
*::-webkit-scrollbar-track-piece {
background-color: #f8f8f8;
-webkit-border-radius: 2em;
-moz-border-radius: 2em;
border-radius: 2em;
}

/* 滚动条的宽度 */
*::-webkit-scrollbar {
width: 9px;
height: 9px;
}

/* 滚动条的设置 */
*::-webkit-scrollbar-thumb {
background-color: #ddd;
background-clip: padding-box;
-webkit-border-radius: 2em;
-moz-border-radius: 2em;
border-radius: 2em;
}

/* 滚动条鼠标移上去 */
*::-webkit-scrollbar-thumb:hover {
background-color: #bbb;
}

+ 40
- 0
src/styles/reset.scss Datei anzeigen

@@ -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
- 0
src/styles/variables.scss Datei anzeigen

@@ -0,0 +1,5 @@
$primaryColor: #316c72;

:root {
--vh100: 100vh;
}

+ 9
- 0
src/utils/cache/index.js Datei anzeigen

@@ -0,0 +1,9 @@
import { createWebStorage } from './web-storage'

export const createLocalStorage = function(option = {}) {
return createWebStorage({ prefixKey: option.prefixKey || '', storage: localStorage })
}

export const createSessionStorage = function(option = {}) {
return createWebStorage({ prefixKey: option.prefixKey || '', storage: sessionStorage })
}

+ 55
- 0
src/utils/cache/web-storage.js Datei anzeigen

@@ -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
- 0
src/utils/http/help.js Datei anzeigen

@@ -0,0 +1,13 @@
import { useUserStore } from '@/store/modules/user'

const WITHOUT_TOKEN_API = [{ url: '/login/login', method: 'POST' }, { url: '/login/captcha', method: 'GET' }]

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
- 0
src/utils/http/index.js Datei anzeigen

@@ -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_MOCK || import.meta.env.VITE_APP_GLOB_BASE_API_MOCK
})

+ 82
- 0
src/utils/http/interceptors.js Datei anzeigen

@@ -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) => {
const { 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
- 0
src/utils/index.js Datei anzeigen

@@ -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) {
const context = this
if (timeout) {
clearTimeout(timeout)
}
// 立即执行需要两个条件,一是immediate为true,二是timeout未被赋值或被置为null
if (immediate) {
/**
* 如果定时器不存在,则立即执行,并设置一个定时器,wait毫秒后将定时器置为null
* 这样确保立即执行后wait毫秒内不会被再次触发
*/
const 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)
}
}
}

+ 110
- 0
src/utils/is.js Datei anzeigen

@@ -0,0 +1,110 @@
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(val) && 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
}

/**
* * 类似sql的isnull函数
* * 第一个参数为null/undefined/''则返回第二个参数作为默认值,否则返回第一个参数
* @param {Number|Boolean|String} val
* @param {Number|Boolean|String} replaceVal
* @returns
*/
export function isNullReplace(val, replaceVal = '') {
return isNullOrUndef(val) || val === '' ? replaceVal : val
}

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

/**
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}

export const isServer = typeof window === 'undefined'

export const isClient = !isServer

+ 17
- 0
src/utils/module.js Datei anzeigen

@@ -0,0 +1,17 @@
// const module = import.meta.glob(`@/views/**/*.vue`)
// const modules = {}
// Object.keys(module).forEach((item) => {
// module[item]().then(res => {
// const moduleName = item.replace(/^\.\/modules\/(.*)\.\w+$/, '$1')
// modules[moduleName] = res?.default
// })
// })

const modulesEager = import.meta.globEager(`@/views/**/*.vue`)
const modules = Object.keys(modulesEager).reduce((modules, path) => {
const moduleName = path.replace(/^\.\/modules\/(.*)\.\w+$/, '$1')
modules[moduleName] = modulesEager[path]?.default
return modules
}, {})

export default modules

+ 18
- 0
src/utils/tags.js Datei anzeigen

@@ -0,0 +1,18 @@
import { createLocalStorage } from './cache'

const STORAGE_CODE = 'tags_menu'
const DURATION = 24 * 60 * 60

export const lsToken = createLocalStorage()

export function getTags() {
return lsToken.get(STORAGE_CODE)
}

export function setTags(token) {
lsToken.set(STORAGE_CODE, token, DURATION)
}

export function removeTags() {
lsToken.remove(STORAGE_CODE)
}

+ 41
- 0
src/utils/token.js Datei anzeigen

@@ -0,0 +1,41 @@
import { createLocalStorage } from './cache'
// import { refreshToken } from '@/api/auth'

const TOKEN_CODE = 'access_token'
const DURATION = 24 * 60 * 60

export const lsToken = createLocalStorage()

/* 获取token */
export function getToken() {
return lsToken.get(TOKEN_CODE)
}

/* 设置token */
export function setToken(token) {
lsToken.set(TOKEN_CODE, token, DURATION)
}

/* 移出token */
export function removeToken() {
lsToken.remove(TOKEN_CODE)
}

/* 刷新token */
// 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)
// }
// }
// }

+ 12
- 0
src/utils/ui/theme.js Datei anzeigen

@@ -0,0 +1,12 @@
const common = {
primaryColor: '#36ad6a'
}

const themeOverrides = {
common,
Button: {
textColor: common.primaryColor
}
}

export default themeOverrides

+ 26
- 0
src/views/dashboard/index.vue Datei anzeigen

@@ -0,0 +1,26 @@
<template>
<div>
首页
<n-button @click="toSystem">跳转</n-button>
</div>
</template>

<script>
import { useRouter } from 'vue-router'
export default {
name: 'HomePage',
setup(props) {
const router = useRouter()
function toSystem() {
router.push({ path: '/login' })
}
return {
toSystem
}
}
}
</script>
<style lang="scss" scoped>

</style>


+ 43
- 0
src/views/login/index.vue Datei anzeigen

@@ -0,0 +1,43 @@
<template>
<div>
<n-button @click="handleLogin">登录</n-button>
</div>
</template>

<script>
import { userLogin, userCaptcha } from '@/api/login/index.js'
import { setToken } from '@/utils/token'
export default {
name: 'LoginPage',
setup() {
async function handleLogin() {
try {
const params = {
captcha: '1',
key: '9132e06a-e2f0-4a9d-88da-b43ced72bb2e',
password: '123456',
remember: false,
username: 'admin'
}
const res = await userLogin(params)
if (res.code === 0) {
setToken(res.data.access_token)
}
} catch (error) {
// console.log(error)
}
}

// userCaptcha()

return {
handleLogin
}
}
}
</script>

<style lang="scss" scoped>

</style>


+ 146
- 0
src/views/system/menu/index.vue Datei anzeigen

@@ -0,0 +1,146 @@
<template>
<div>
<data-table :columns="data.columns" :data="data.data" size="large">
<template #tableTitle>
<n-button type="primary">
添加角色
</n-button>
</template>
</data-table>

</div>
</template>

<script>
import dataTable from '@/components/DataTable/index.vue'
import Action from '@/components/DataTable/tools/action.vue'
import { getMenuList } from '@/api/system/index.js'
import { h, onMounted } from 'vue'
import { reactive } from 'vue'
export default {
name: 'MenuPage',
components: { dataTable },
setup() {
const data = reactive({
columns: [
{
title: '菜单标题',
key: 'title',
align: 'center',
width: 200
},
{
title: '菜单类型',
key: 'type',
align: 'center',
width: 100
},
{
title: '请求方式',
key: 'method',
align: 'center',
width: 100
},
{
title: '路由地址',
key: 'path',
align: 'center',
width: 200
},
{
title: '组件路径',
key: 'component',
align: 'center',
width: 200
},
{
title: '权限标识',
key: 'permission',
align: 'center',
width: 200
},
{
title: '状态',
key: 'status',
align: 'center',
width: 100
},
{
title: '排序',
key: 'sort',
align: 'center',
width: 100
},
{
title: '可见',
key: 'hide',
align: 'center',
width: 100
},
{
title: '创建时间',
key: 'createTime',
align: 'center',
width: 160
},
{
title: '操作',
align: 'center',
width: 150,
fixed: 'right',
render(row) {
return h(Action, {
actions: [
{
label: '添加',
type: 'button',
props: {
type: 'primary',
onClick: play.bind(null, row)
},
auth: 'basic_list'
},
{
label: '修改',
auth: 'basic_list'
},
{
label: '删除',
type: 'popconfirm',
auth: 'basic_list'
}
],
align: 'center'
})
}
}
],
data: [

]
})

function play(row) {
console.log(row)
}

/**
* @description: 获取菜单数据
* @return {*}
*/
async function fetchList() {
const res = await getMenuList()
data.data = res.data
}

onMounted(() => {
fetchList()
})

return { data }
}
}

</script>
<style scoped lang='scss'>
</style>

+ 17
- 0
src/views/system/role/index.vue Datei anzeigen

@@ -0,0 +1,17 @@
<template>
<div>
1
</div>
</template>

<script>
export default {
name: '',
setup() {

}
}

</script>
<style scoped lang='scss'>
</style>

+ 162
- 0
src/views/system/user/index.vue Datei anzeigen

@@ -0,0 +1,162 @@
<template>
<div>
<data-table :columns="data.columns" :data="data.data" :pagination="data.pagination" size="large" scroll-x="1200">
<template #tableTitle>
<n-button type="primary">
新建
</n-button>
<n-button type="primary">
删除
</n-button>
</template>
</data-table>

</div>
</template>

<script>
import dataTable from '@/components/DataTable/index.vue'
import TableAction from '@/components/DataTable/tools/Action.vue'
import TableImage from '@/components/DataTable/tools/Image.vue'
import { getUserList } from '@/api/system/index.js'
import { h, onMounted } from 'vue'
import { reactive } from 'vue'
export default {
name: 'MenuPage',
components: { dataTable },
setup() {
const data = reactive({
columns: [
{
title: '用户编号',
key: 'code',
align: 'center'
},
{
title: '头像',
key: 'avatar',
align: 'center',
render(row) {
return h(TableImage, {
images: {
width: 36,
height: 36,
src: row.avatar
}
})
}
},
{
title: '用户账号',
key: 'username',
align: 'center'
},
{
title: '用户姓名',
key: 'realname',
align: 'center'
},
{
title: '用户类型',
key: 'type',
align: 'center',
width: 100
},
{
title: '角色',
key: 'roles',
align: 'center'

},
{
title: '状态',
key: 'status',
align: 'center',
width: 100
},
{
title: '部门',
key: 'deptName',
align: 'center'
},
{
title: '创建时间',
key: 'createTime',
align: 'center',
width: 160
},
{
title: '更新时间',
key: 'updateTime',
align: 'center',
width: 160
},
{
title: '操作',
align: 'center',
width: 150,
fixed: 'right',
render(row) {
return h(TableAction, {
actions: [
{
label: '添加',
type: 'button',
props: {
type: 'primary',
onClick: play.bind(null, row)
},
auth: 'basic_list'
},
{
label: '修改',
auth: 'basic_list'
},
{
label: '删除',
type: 'popconfirm',
auth: 'basic_list'
}
],
align: 'center'
})
}
}
],
data: [],
pagination: {
pageSize: 10
}
})

function play(row) {
console.log(row)
}

/**
* @description: 获取用户数据
* @return {*}
*/
async function fetchList() {
const params = {
page: 1,
limit: 10
}
const res = await getUserList(params)
data.data = res.data
}

onMounted(() => {
fetchList()
})

return { data }
}
}

</script>
<style scoped lang='scss'>
.n-button+.n-button{
margin-left: 10px;
}
</style>

+ 49
- 0
vite.config.js Datei anzeigen

@@ -0,0 +1,49 @@
import { defineConfig, loadEnv } from 'vite'
import { resolve } 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'

function pathResolve(dir) {
return resolve(__dirname, '.', dir)
}

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 || '/',
plugins: createVitePlugins(viteEnv, isBuild),
lintOnSave: false,
resolve: {
alias: {
'@': pathResolve('src')
}
},

css: {
preprocessorOptions: {
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
}
}
})

Laden…
Abbrechen
Speichern