@@ -0,0 +1,5 @@ | |||
# title | |||
VITE_APP_TITLE = '河湖长眼' | |||
# 端口号 | |||
VITE_PORT = 3000 |
@@ -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' |
@@ -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' |
@@ -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' |
@@ -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' |
@@ -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'] | |||
} | |||
} | |||
@@ -0,0 +1,5 @@ | |||
node_modules | |||
.DS_Store | |||
dist | |||
dist-ssr | |||
*.local |
@@ -0,0 +1,3 @@ | |||
{ | |||
"recommendations": ["johnsoncodehk.volar"] | |||
} |
@@ -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) |
@@ -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' |
@@ -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)) | |||
} | |||
} |
@@ -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 }) | |||
} |
@@ -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() |
@@ -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) | |||
} |
@@ -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 | |||
} |
@@ -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 | |||
} |
@@ -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(); | |||
` | |||
}) | |||
} |
@@ -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()], | |||
}) | |||
} |
@@ -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 | |||
} |
@@ -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> |
@@ -0,0 +1,9 @@ | |||
{ | |||
"compilerOptions": { | |||
"baseUrl": "./", | |||
"paths": { | |||
"@/*": ["src/*"] | |||
} | |||
}, | |||
"exclude": ["node_modules", "dist"] | |||
} |
@@ -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) | |||
} |
@@ -0,0 +1,10 @@ | |||
import Mock from 'mockjs' | |||
export function resultSuccess(data, { message = 'ok' } = {}) { | |||
return Mock.mock({ | |||
code: 0, | |||
data, | |||
message, | |||
type: 'success' | |||
}) | |||
} |
@@ -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 }) | |||
} | |||
} | |||
] |
@@ -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) | |||
} | |||
} | |||
] |
@@ -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 | |||
} |
@@ -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" | |||
} | |||
} |
@@ -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> |
@@ -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', | |||
}) | |||
} |
@@ -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' | |||
}) | |||
} |
@@ -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', | |||
}) | |||
} |
@@ -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 | |||
}) | |||
} |
@@ -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, | |||
}) | |||
} |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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] | |||
} | |||
} | |||
} | |||
} |
@@ -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 | |||
} | |||
} |
@@ -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 } | |||
} |
@@ -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> |
@@ -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> | |||
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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() |
@@ -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) | |||
} |
@@ -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() | |||
}) | |||
} |
@@ -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 | |||
} | |||
}) | |||
} |
@@ -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 }}) | |||
} | |||
} | |||
}) | |||
} |
@@ -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) | |||
} |
@@ -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 } |
@@ -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: '菜单管理' | |||
} | |||
} | |||
] | |||
} | |||
] | |||
@@ -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 |
@@ -0,0 +1,5 @@ | |||
import { createPinia } from 'pinia' | |||
export function setupStore(app) { | |||
app.use(createPinia()) | |||
} |
@@ -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' | |||
} | |||
} | |||
} | |||
} | |||
}) |
@@ -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) | |||
} | |||
} | |||
} | |||
}) |
@@ -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: { | |||
} | |||
}) |
@@ -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) | |||
} | |||
} | |||
}) |
@@ -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 } | |||
} | |||
} | |||
}) |
@@ -0,0 +1,3 @@ | |||
@import './reset.scss'; | |||
@import './public.scss'; | |||
@import './layout.scss'; |
@@ -0,0 +1,12 @@ | |||
.layout,.n-space{ | |||
height: inherit; | |||
} | |||
.layout{ | |||
.layout__header{ | |||
height: 48px; | |||
} | |||
.layout__content{ | |||
height: calc(100vh - 48px); | |||
} | |||
} |
@@ -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; | |||
} |
@@ -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; | |||
} |
@@ -0,0 +1,5 @@ | |||
$primaryColor: #316c72; | |||
:root { | |||
--vh100: 100vh; | |||
} |
@@ -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 }) | |||
} |
@@ -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 }) | |||
} |
@@ -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 | |||
} | |||
} |
@@ -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 | |||
}) |
@@ -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: '接口异常,请稍后重试!' }) | |||
} | |||
} | |||
) | |||
} |
@@ -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) | |||
} | |||
} | |||
} |
@@ -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 |
@@ -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 |
@@ -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) | |||
} |
@@ -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) | |||
// } | |||
// } | |||
// } |
@@ -0,0 +1,12 @@ | |||
const common = { | |||
primaryColor: '#36ad6a' | |||
} | |||
const themeOverrides = { | |||
common, | |||
Button: { | |||
textColor: common.primaryColor | |||
} | |||
} | |||
export default themeOverrides |
@@ -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> | |||
@@ -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> | |||
@@ -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> |
@@ -0,0 +1,17 @@ | |||
<template> | |||
<div> | |||
1 | |||
</div> | |||
</template> | |||
<script> | |||
export default { | |||
name: '', | |||
setup() { | |||
} | |||
} | |||
</script> | |||
<style scoped lang='scss'> | |||
</style> |
@@ -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> |
@@ -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 | |||
} | |||
} | |||
}) |