init
This commit is contained in:
commit
e0b93a88c2
|
|
@ -0,0 +1,8 @@
|
|||
# title
|
||||
VITE_APP_TITLE = '智飞'
|
||||
|
||||
# 端口号
|
||||
VITE_PORT = 3050
|
||||
|
||||
|
||||
VITE_SERVER = "/pilot/admin"
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# 资源公共路径,需要以 /开头和结尾
|
||||
VITE_PUBLIC_PATH = '/'
|
||||
|
||||
# 是否启用MOCK
|
||||
VITE_APP_USE_MOCK = false
|
||||
|
||||
# proxy
|
||||
VITE_PROXY = [["/api-local","http://127.0.0.1:8002"],["/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,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,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
|
||||
<link rel="stylesheet" href="https://g.alicdn.com/de/prismplayer/2.9.21/skins/default/aliplayer-min.css" />
|
||||
<script charset="utf-8" type="text/javascript" src="https://g.alicdn.com/de/prismplayer/2.9.21/aliplayer-h5-min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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'
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"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": {
|
||||
"@tinymce/tinymce-vue": "^4.0.5",
|
||||
"@vicons/antd": "^0.10.0",
|
||||
"@vicons/ionicons5": "^0.10.0",
|
||||
"ali-oss": "^6.17.1",
|
||||
"axios": "^0.26.1",
|
||||
"dayjs": "^1.11.2",
|
||||
"mockjs": "^1.1.0",
|
||||
"pinia": "^2.0.13",
|
||||
"pinia-plugin-persist": "^1.0.0",
|
||||
"tinymce": "^5.10.2",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
|
|
@ -0,0 +1,42 @@
|
|||
<!--
|
||||
* @Author: whyafterme
|
||||
* @Date: 2022-10-19 11:22:44
|
||||
* @LastEditTime: 2022-10-19 11:31:00
|
||||
* @LastEditors: whyafterme
|
||||
* @Description:
|
||||
* @FilePath: \gis\src\App.vue
|
||||
-->
|
||||
<template>
|
||||
<n-config-provider :locale="zhCN" :date-locale="dateZhCN" inline-theme-disabled :theme-overrides="themeOverrides">
|
||||
<n-loading-bar-provider>
|
||||
<loading-bar />
|
||||
<n-dialog-provider>
|
||||
<dialog-content />
|
||||
<n-message-provider>
|
||||
<message-content />
|
||||
</n-message-provider>
|
||||
<router-view v-slot="{ Component }">
|
||||
<component :is="Component" />
|
||||
</router-view>
|
||||
</n-dialog-provider>
|
||||
</n-loading-bar-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { zhCN, dateZhCN } from 'naive-ui'
|
||||
import themeOverrides from '@/utils/ui/theme.js'
|
||||
import loadingBar from '@/components/LoadingBar/index.vue'
|
||||
import messageContent from '@/components/Message/index.vue'
|
||||
import dialogContent from '@/components/Dialog/index.vue'
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#app {
|
||||
height: 100%;
|
||||
.n-config-provider {
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
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'
|
||||
})
|
||||
}
|
||||
|
||||
export function getMenu() {
|
||||
return request({
|
||||
url: '/index/getMenuList',
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { defAxios as request } from '@/utils/http'
|
||||
|
||||
/**
|
||||
* @description: 获取音视频上传地址和凭证
|
||||
* @param {Object} params
|
||||
* @return {Object}
|
||||
*/
|
||||
export function getAuth(params) {
|
||||
return request({
|
||||
url: '/aliyuncsVod/createUploadVideo',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 刷新音/视频上传凭证
|
||||
* @param {String} videoId
|
||||
* @return {*}
|
||||
*/
|
||||
export function refreshAuth(videoId) {
|
||||
return request({
|
||||
url: '/aliyuncsVod/refreshUploadVideo',
|
||||
method: 'get',
|
||||
params: {
|
||||
videoId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 获取图片上传鉴权
|
||||
* @param {String} objectName
|
||||
* @return {*}
|
||||
*/
|
||||
export function getOssAuth(objectName) {
|
||||
return request({
|
||||
url: '/aliyunOss/getSecurityToken',
|
||||
method: 'get',
|
||||
responseAll: true,
|
||||
params: {
|
||||
objectName
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { defAxios as request } from '@/utils/http'
|
||||
export function updatePwd(data) {
|
||||
return request({
|
||||
url: '/index/updatePwd',
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { defAxios as request } from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 登录接口
|
||||
* @param {Object} 用户名以及密码
|
||||
* @returns 返回token信息
|
||||
*/
|
||||
export function userLogin(data = {}) {
|
||||
return request({
|
||||
url: '/login/login',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* params
|
||||
* @returns 当前登录人信息
|
||||
*/
|
||||
export function getUser() {
|
||||
return request({
|
||||
url: '/index/getUserInfo',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
* @returns 验证码图片
|
||||
*/
|
||||
export function userCaptcha() {
|
||||
return request({
|
||||
url: '/login/captcha',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
* @returns
|
||||
*/
|
||||
export function loginOut() {
|
||||
return request({
|
||||
url: '/login/logout',
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { defAxios as request } from '@/utils/http'
|
||||
|
||||
export function getUsers(data = {}) {
|
||||
return request({
|
||||
url: '/users',
|
||||
method: 'get',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getUser() {
|
||||
return request({
|
||||
url: '/index/getUserInfo',
|
||||
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,53 @@
|
|||
<template>
|
||||
<n-cascader v-bind="getProps" @update:value="handleUpdate" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NCascader } from 'naive-ui'
|
||||
import { computed } from 'vue'
|
||||
export default {
|
||||
name: 'AreaCascader',
|
||||
props: {
|
||||
...NCascader.props,
|
||||
showCheckbox: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
field: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return ['province', 'city', 'distance']
|
||||
}
|
||||
}
|
||||
},
|
||||
emits: ['selectd'],
|
||||
setup(props, { emit }) {
|
||||
const getProps = computed(() => {
|
||||
return {
|
||||
...props
|
||||
}
|
||||
})
|
||||
function handleUpdate(value, option, pathValues) {
|
||||
const field = props.field
|
||||
const valueField = props.valueField
|
||||
const filedJson = {}
|
||||
field.forEach((item, index) => {
|
||||
filedJson[item] = pathValues?.[index]?.[valueField] || ''
|
||||
})
|
||||
emit('selectd', filedJson)
|
||||
}
|
||||
function clearValue() {
|
||||
handleUpdate(null, null, [])
|
||||
}
|
||||
|
||||
return {
|
||||
getProps,
|
||||
handleUpdate,
|
||||
clearValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped lang='scss'>
|
||||
</style>
|
||||
|
|
@ -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,59 @@
|
|||
<template>
|
||||
<n-modal
|
||||
v-bind="getModalOptions"
|
||||
:style="`width:${getModalOptions.width}px`"
|
||||
preset="card"
|
||||
:title="options.title"
|
||||
@update:show="handleClose"
|
||||
>
|
||||
<n-card :bordered="false">
|
||||
<slot name="Context" />
|
||||
<n-space style="float: right">
|
||||
<n-button @click="handleClose">取消</n-button>
|
||||
<n-button type="primary" @click="handleClick">确认</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { defineComponent, computed, unref } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'Modal',
|
||||
props: {
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
emits: {
|
||||
save: null, // click事件没有检验
|
||||
onClose: (value) => {
|
||||
return value
|
||||
}
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const getModalOptions = computed(() => {
|
||||
return {
|
||||
...props.options,
|
||||
width: props.options.width || 600
|
||||
}
|
||||
})
|
||||
const handleClick = function() {
|
||||
emit('save')
|
||||
}
|
||||
const handleClose = function() {
|
||||
emit('onClose', true)
|
||||
}
|
||||
return {
|
||||
getModalOptions,
|
||||
handleClick,
|
||||
handleClose
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang='scss'>
|
||||
</style>
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
<template>
|
||||
<div class="table-toolbar">
|
||||
<!--顶部左侧区域-->
|
||||
<div class="table-toolbar-left">
|
||||
<slot name="tableTitle" />
|
||||
</div>
|
||||
|
||||
<div class="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>刷新</span>
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="s-table">
|
||||
<n-data-table
|
||||
ref="tableElRef"
|
||||
v-bind="getBindProps"
|
||||
:pagination="pagination"
|
||||
@update:page="updatePage"
|
||||
@update:page-size="updatePageSize"
|
||||
>
|
||||
<template #empty>
|
||||
<slot name="empty" />
|
||||
</template>
|
||||
</n-data-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ReloadOutlined } from '@vicons/antd'
|
||||
import { tableProps } from './tools/props.js'
|
||||
import { useDataSource } from './tools/useDataSource.js'
|
||||
import { usePagination } from './tools/usePagination.js'
|
||||
import { ref, unref, computed, toRaw, provide } from 'vue'
|
||||
export default {
|
||||
name: 'DataTable',
|
||||
components: { ReloadOutlined },
|
||||
props: {
|
||||
...tableProps
|
||||
},
|
||||
emits: [
|
||||
'fetch-success',
|
||||
'fetch-error',
|
||||
'update:checked-row-keys',
|
||||
'edit-end',
|
||||
'edit-cancel',
|
||||
'edit-row-end',
|
||||
'edit-change'
|
||||
],
|
||||
setup(props, { emit }) {
|
||||
const getProps = computed(() => {
|
||||
return { ...props }
|
||||
})
|
||||
|
||||
/* loading--start */
|
||||
const loadingRef = ref(unref(getProps).loading)
|
||||
const getLoading = computed(() => unref(loadingRef))
|
||||
function setLoading(loading) {
|
||||
loadingRef.value = loading
|
||||
}
|
||||
/* loading--end */
|
||||
|
||||
/* pagination-start */
|
||||
const { getPaginationInfo, setPagination } = usePagination(getProps)
|
||||
const pagination = computed(() => toRaw(unref(getPaginationInfo)))
|
||||
|
||||
/* 页码切换 */
|
||||
function updatePage(page) {
|
||||
setPagination({ page: page })
|
||||
reload()
|
||||
}
|
||||
|
||||
/* 分页数量切换 */
|
||||
function updatePageSize(size) {
|
||||
setPagination({ page: 1, pageSize: size })
|
||||
reload()
|
||||
}
|
||||
/* pagination-end */
|
||||
|
||||
/* tableData-start */
|
||||
const tableData = ref([])
|
||||
const { getDataSourceRef, getRowKey, reload, reFetch } = useDataSource(getProps, { getPaginationInfo, setPagination, tableData, setLoading }, emit)
|
||||
const isRequest = !!unref(getProps).request
|
||||
const getBindProps = computed(() => {
|
||||
return {
|
||||
...unref(getProps),
|
||||
loading: unref(getLoading),
|
||||
rowKey: unref(getRowKey),
|
||||
data: isRequest ? unref(getDataSourceRef) : unref(getProps).data,
|
||||
remote: true
|
||||
}
|
||||
})
|
||||
|
||||
emit('fetch-success', isRequest ? unref(getDataSourceRef) : unref(getProps).data)
|
||||
|
||||
const key = Symbol('s-table')
|
||||
provide(key, { getBindProps })
|
||||
/* tableData-end */
|
||||
|
||||
return {
|
||||
getBindProps,
|
||||
pagination,
|
||||
updatePage,
|
||||
updatePageSize,
|
||||
reload,
|
||||
reFetch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</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;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
.table-toolbar-right-icon {
|
||||
margin-left: 12px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
.n-icon{
|
||||
vertical-align: middle;
|
||||
}
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.table-toolbar-inner-popover-title {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<div v-if="getIsShow" 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
|
||||
}
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const data = reactive({
|
||||
permissionList: [
|
||||
'basic_list'
|
||||
]
|
||||
})
|
||||
const getActions = computed(() => {
|
||||
return (toRaw(props.actions) || [])
|
||||
.filter((action) => {
|
||||
if (!Object.keys(action).includes('show')) {
|
||||
action.show = Object.keys(action).includes('hidden') ? !action.hidden : true
|
||||
}
|
||||
return (data.permissionList.includes(action.auth) || action.auth === '') && action.show
|
||||
})
|
||||
})
|
||||
|
||||
const getAlign = computed(() => {
|
||||
return toRaw(props.align)
|
||||
})
|
||||
|
||||
const getIsShow = computed(() => {
|
||||
return toRaw(props.show)
|
||||
})
|
||||
|
||||
return {
|
||||
getActions,
|
||||
getAlign,
|
||||
getIsShow
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</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,44 @@
|
|||
<template>
|
||||
<n-switch v-bind="getSwitchProps" v-model:value="switchVlue" @update:value="changeValue" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref, unref, computed } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'TableSwitch',
|
||||
props: {
|
||||
data: {
|
||||
type: [Object, String, Number, Boolean],
|
||||
required: true
|
||||
},
|
||||
rowKey: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['change'],
|
||||
setup(props, { emit }) {
|
||||
const switchVlue = ref()
|
||||
const { data, rowKey } = unref(props)
|
||||
switchVlue.value = rowKey ? data[rowKey] : data
|
||||
const getSwitchProps = computed(() => {
|
||||
return {
|
||||
...unref(props)
|
||||
}
|
||||
})
|
||||
function changeValue(value) {
|
||||
const { data } = props
|
||||
const params = {
|
||||
data,
|
||||
value
|
||||
}
|
||||
emit('change', params)
|
||||
}
|
||||
return {
|
||||
switchVlue,
|
||||
getSwitchProps,
|
||||
changeValue
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
<template>
|
||||
<template v-if="isFilter">
|
||||
<n-tag
|
||||
v-for="(item,index) in getData.data"
|
||||
:key="`tag_${index}`"
|
||||
v-bind="getProps"
|
||||
:color="getFilter(item[getData.rowKey])?.color || getProps?.color"
|
||||
>
|
||||
{{ getFilter(item[getData.rowKey]).label }}
|
||||
</n-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<n-tag v-for="(item,index) in getData.data" :key="`tag_${index}`" v-bind="getProps">
|
||||
{{ item[getData.rowKey] }}
|
||||
</n-tag>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, computed, unref } from 'vue'
|
||||
import { isArray } from '@/utils/is.js'
|
||||
export default defineComponent({
|
||||
name: 'TableTags',
|
||||
props: {
|
||||
/* 展示的数据 */
|
||||
data: {
|
||||
type: [Array, String, Number],
|
||||
required: true
|
||||
},
|
||||
/* 展示数据取的字段 */
|
||||
rowKey: {
|
||||
type: String,
|
||||
default: 'name'
|
||||
},
|
||||
/* 过滤的数据 */
|
||||
// filters: [
|
||||
// {
|
||||
// key: '',
|
||||
// label: '',
|
||||
// color: {}
|
||||
// }
|
||||
// ],
|
||||
filters: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
/* tag标签的属性 */
|
||||
tags: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const isFilter = computed(() => {
|
||||
return !!(props.filters)
|
||||
})
|
||||
const { filters } = unref(props)
|
||||
function getFilter(value) {
|
||||
const data = filters.find(item => {
|
||||
return item.value === value
|
||||
})
|
||||
return data || {
|
||||
value: value,
|
||||
label: value
|
||||
}
|
||||
}
|
||||
/* 获取传递的数据 */
|
||||
const getData = computed(() => {
|
||||
return {
|
||||
rowKey: unref(props.rowKey),
|
||||
data: isArray(props.data) ? { ...unref(props.data) } : [{ [props.rowKey]: props.data }],
|
||||
filters: { ...unref(props.filters) }
|
||||
}
|
||||
})
|
||||
/* 获取tags的属性 */
|
||||
const getProps = computed(() => {
|
||||
return {
|
||||
...unref(props.tags),
|
||||
closable: false,
|
||||
bordered: props.tags?.bordered || false
|
||||
}
|
||||
})
|
||||
return {
|
||||
isFilter,
|
||||
getFilter,
|
||||
getData,
|
||||
getProps
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang='scss'>
|
||||
.n-tag{
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { NDataTable } from 'naive-ui'
|
||||
|
||||
export const tableProps = {
|
||||
...NDataTable.props,
|
||||
/* 初始化接口请求 */
|
||||
request: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
/* 分页信息 */
|
||||
pagination: {
|
||||
type: [Object, Boolean],
|
||||
default: () => {}
|
||||
},
|
||||
/* 数据格式 */
|
||||
dataType: {
|
||||
type: String,
|
||||
default: 'flat',
|
||||
validator: (value) => {
|
||||
return ['flat', 'tree'].indexOf(value) !== -1
|
||||
}
|
||||
},
|
||||
/* 分页设置信息 */
|
||||
paginationSetting: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
// 当前页的字段名
|
||||
pageField: 'page',
|
||||
// 每页数量字段名
|
||||
sizeField: 'limit',
|
||||
// 接口返回的字段名
|
||||
listPageField: 'current',
|
||||
// 接口返回的数据字段名
|
||||
listField: 'records',
|
||||
// 接口返回总页数字段名
|
||||
totalField: 'total',
|
||||
// 默认分页数量
|
||||
pageSize: 10,
|
||||
// 可切换每页数量集合
|
||||
pageSizes: [10, 20, 30, 40, 50],
|
||||
// 是否显示每页条数的选择器
|
||||
showSizePicker: false,
|
||||
// 是否显示快速跳转
|
||||
showQuickJumper: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* pid形式数据转children形式
|
||||
* @param data 需要转换的数组
|
||||
* @param idKey id字段名
|
||||
* @param pidKey pid字段名
|
||||
* @param childKey 生成的children字段名
|
||||
* @param pid 顶级的pid
|
||||
* @param addPIds 是否添加所有父级id的字段
|
||||
* @param parentsKey 所有父级id的字段名称,默认parentIds
|
||||
* @param parentIds 所有父级id
|
||||
* @returns {[]}
|
||||
*/
|
||||
export function toTreeData(data, idKey, pidKey, childKey, pid, addPIds, parentsKey, parentIds) {
|
||||
if (typeof data === 'object' && !Array.isArray(data)) {
|
||||
idKey = data.idKey
|
||||
pidKey = data.pidKey
|
||||
childKey = data.childKey
|
||||
pid = data.pid
|
||||
addPIds = data.addPIds
|
||||
parentsKey = data.parentsKey
|
||||
parentIds = data.parentIds
|
||||
data = data.data
|
||||
}
|
||||
if (!childKey) {
|
||||
childKey = 'children'
|
||||
}
|
||||
if (typeof pid === 'undefined') {
|
||||
pid = []
|
||||
data.forEach((d) => {
|
||||
let flag = true
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (d[pidKey] === data[i][idKey]) {
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (flag) {
|
||||
pid.push(d[pidKey])
|
||||
}
|
||||
})
|
||||
}
|
||||
const result = []
|
||||
data.forEach((d) => {
|
||||
if (d[idKey] === d[pidKey]) {
|
||||
console.error('data error: ', d)
|
||||
return
|
||||
}
|
||||
if (Array.isArray(pid) ? (pid.indexOf(d[pidKey]) !== -1) : (d[pidKey] === pid)) {
|
||||
const children = toTreeData({
|
||||
data: data,
|
||||
idKey: idKey,
|
||||
pidKey: pidKey,
|
||||
childKey: childKey,
|
||||
pid: d[idKey],
|
||||
addPIds: addPIds,
|
||||
parentsKey: parentsKey,
|
||||
parentIds: (parentIds || []).concat([d[idKey]])
|
||||
})
|
||||
if (children.length > 0) {
|
||||
d[childKey] = children
|
||||
}
|
||||
if (addPIds) {
|
||||
d[parentsKey || 'parentIds'] = parentIds || []
|
||||
}
|
||||
result.push(d)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import { ref, unref, computed, onMounted } from 'vue'
|
||||
import { isBoolean } from '@/utils/is'
|
||||
import { toTreeData } from './toTree'
|
||||
|
||||
export function useDataSource(propsRef, { getPaginationInfo, setPagination, setLoading, tableData }, emit) {
|
||||
const dataSourceRef = ref([])
|
||||
const paginationPage = ref(1)
|
||||
async function fetch(opt) {
|
||||
try {
|
||||
/* 设置loading */
|
||||
setLoading(true)
|
||||
const { request, pagination, paginationSetting, dataType } = unref(propsRef)
|
||||
/* 无接口请求中断 */
|
||||
if (!request) return
|
||||
/* 获取分页信息 */
|
||||
const pageField = paginationSetting.pageField
|
||||
const listPageField = paginationSetting.listPageField
|
||||
const sizeField = paginationSetting.sizeField
|
||||
const totalField = paginationSetting.totalField
|
||||
const listField = paginationSetting.listField
|
||||
let pageParams = {}
|
||||
const { page = 1, pageSize = 10 } = unref(getPaginationInfo)
|
||||
/* 判断是否需要分页信息 */
|
||||
const noPagination = (isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)
|
||||
if (noPagination) {
|
||||
pageParams = {}
|
||||
} else {
|
||||
pageParams[pageField] = (opt && opt[pageField]) || page
|
||||
paginationPage.value = pageParams[pageField]
|
||||
pageParams[sizeField] = pageSize
|
||||
}
|
||||
const params = {
|
||||
...pageParams
|
||||
}
|
||||
const response = await request(params)
|
||||
const res = noPagination ? response : response.data
|
||||
const resultTotal = res[totalField] || 0
|
||||
const currentPage = res[listPageField]
|
||||
// 如果数据异常,需获取正确的页码再次执行
|
||||
if (resultTotal) {
|
||||
if (page > Math.ceil(resultTotal / pageSize)) {
|
||||
setPagination({
|
||||
[pageField]: Math.ceil(resultTotal / pageSize)
|
||||
})
|
||||
fetch(opt)
|
||||
}
|
||||
}
|
||||
// 处理数据结构
|
||||
const resultInfo = res[listField] ? res[listField] : res
|
||||
dataSourceRef.value = dataType === 'tree' ? dealTree(resultInfo.data) : resultInfo
|
||||
setPagination({
|
||||
[pageField]: currentPage,
|
||||
[totalField]: Math.ceil(resultTotal / pageSize),
|
||||
itemCount: 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历数据处理成树形结构
|
||||
* @returns 返回树形结构数据
|
||||
*/
|
||||
function dealTree(info) {
|
||||
const tree = toTreeData(info, 'id', 'pid', 'children')
|
||||
return tree
|
||||
}
|
||||
|
||||
const getDataSourceRef = computed(() => {
|
||||
const dataSource = unref(dataSourceRef)
|
||||
if (!dataSource || dataSource.length === 0) {
|
||||
return unref(dataSourceRef)
|
||||
}
|
||||
return unref(dataSourceRef)
|
||||
})
|
||||
|
||||
function getDataSource() {
|
||||
return getDataSourceRef.value
|
||||
}
|
||||
|
||||
function setTableData(values) {
|
||||
dataSourceRef.value = values
|
||||
}
|
||||
|
||||
const getRowKey = computed(() => {
|
||||
const { rowKey } = unref(propsRef)
|
||||
return rowKey || (() => {
|
||||
return 'key'
|
||||
})
|
||||
})
|
||||
|
||||
async function reload(opt) {
|
||||
await fetch(opt)
|
||||
}
|
||||
|
||||
async function reFetch(opt, reload = true) {
|
||||
const { paginationSetting } = unref(propsRef)
|
||||
const pageField = paginationSetting.pageField
|
||||
const sizeField = paginationSetting.sizeField
|
||||
const pageSize = paginationSetting.pageSize
|
||||
setPagination({
|
||||
[pageField]: reload ? 1 : paginationPage.value,
|
||||
[sizeField]: pageSize
|
||||
})
|
||||
await fetch(opt)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
fetch()
|
||||
}, 15)
|
||||
})
|
||||
|
||||
return {
|
||||
fetch,
|
||||
getDataSourceRef,
|
||||
getDataSource,
|
||||
setTableData,
|
||||
getRowKey,
|
||||
reload,
|
||||
reFetch
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { computed, unref, ref } from 'vue'
|
||||
import { isBoolean } from '@/utils/is'
|
||||
|
||||
export function usePagination(refProps) {
|
||||
const configRef = ref({})
|
||||
const getPaginationInfo = computed(() => {
|
||||
const { pagination, paginationSetting } = unref(refProps)
|
||||
/* 判断是否需要展示分页 */
|
||||
if ((isBoolean(pagination) && !pagination)) {
|
||||
return false
|
||||
}
|
||||
/* 返回配置的分页信息 */
|
||||
return {
|
||||
pageSize: paginationSetting.pageSize,
|
||||
pageSizes: paginationSetting.pageSizes,
|
||||
showSizePicker: paginationSetting.showSizePicker,
|
||||
showQuickJumper: paginationSetting.showQuickJumper,
|
||||
...(isBoolean(pagination) ? {} : pagination),
|
||||
...unref(configRef),
|
||||
pageCount: unref(configRef)[paginationSetting.totalField],
|
||||
prefix({ itemCount }) {
|
||||
return `共 ${itemCount} 条`
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function setPagination(info) {
|
||||
const paginationInfo = unref(getPaginationInfo)
|
||||
configRef.value = {
|
||||
...(!isBoolean(paginationInfo) ? paginationInfo : {}),
|
||||
...info
|
||||
}
|
||||
}
|
||||
|
||||
function getPagination() {
|
||||
return unref(getPaginationInfo)
|
||||
}
|
||||
|
||||
return { getPaginationInfo, setPagination, getPagination }
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<div />
|
||||
</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)) {
|
||||
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,112 @@
|
|||
<template>
|
||||
<n-dropdown
|
||||
trigger="click"
|
||||
:options="getOptions"
|
||||
class="dropdown-icon"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<n-input placeholder="" readonly>
|
||||
<template #prefix>
|
||||
<n-icon
|
||||
v-for="(item,index) in getOptions"
|
||||
v-show="item.key === selectIcon"
|
||||
:key="index"
|
||||
color="#111111"
|
||||
:component="item.component"
|
||||
size="24"
|
||||
/>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reactive, toRefs, h, computed } from 'vue'
|
||||
import { NIcon } from 'naive-ui'
|
||||
import Icons from './tools/icon.js'
|
||||
export default {
|
||||
name: 'IconBoard',
|
||||
props: {
|
||||
selected: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['update:selected'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const data = reactive({
|
||||
selectIcon: props.selected
|
||||
})
|
||||
const renderOption = ({ node, option }) => {
|
||||
return h(NIcon, {
|
||||
class: 'board-icon',
|
||||
size: 24,
|
||||
onClick: () => {
|
||||
console.log(option.key)
|
||||
}
|
||||
},
|
||||
{ default: () => h(option.key) })
|
||||
}
|
||||
|
||||
const getOptions = computed(() => {
|
||||
const options = Icons.map((item, index) => {
|
||||
const object = {
|
||||
key: `icon_${item.name}`,
|
||||
component: item,
|
||||
icon: function icon() {
|
||||
return h(NIcon, {
|
||||
class: 'board-icon',
|
||||
color: '#111111',
|
||||
size: 24 }, {
|
||||
default: () => h(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
return object
|
||||
})
|
||||
return options
|
||||
})
|
||||
|
||||
const nodeProps = (option) => {
|
||||
return {
|
||||
class: 'dropdown__icon',
|
||||
style: {
|
||||
display: 'inline-block'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(key) {
|
||||
data.selectIcon = key
|
||||
emit('update:selected', key)
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(data),
|
||||
Icons,
|
||||
renderOption,
|
||||
getOptions,
|
||||
nodeProps,
|
||||
handleSelect
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<style lang='scss'>
|
||||
.v-binder-follower-content{
|
||||
.dropdown-icon{
|
||||
width: 510px;
|
||||
padding: 5px 15px;
|
||||
.n-dropdown-option{
|
||||
display: inline-block;
|
||||
padding: 6px;
|
||||
}
|
||||
.n-dropdown-option-body__suffix{
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* @Author: whyafterme
|
||||
* @Date: 2022-06-16 11:39:28
|
||||
* @LastEditTime: 2022-06-29 13:51:40
|
||||
* @LastEditors: whyafterme
|
||||
* @Description:
|
||||
* @FilePath: \web\src\components\IconBoard\tools\icon.js
|
||||
*/
|
||||
import {
|
||||
EditOutlined, LogoutOutlined
|
||||
} from '@vicons/antd'
|
||||
|
||||
const Icons = [
|
||||
EditOutlined,
|
||||
LogoutOutlined
|
||||
]
|
||||
|
||||
export default Icons
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<template>
|
||||
<div>
|
||||
<n-upload
|
||||
:default-file-list="fileList"
|
||||
v-bind="getImgOptions"
|
||||
:on-change="handleChange"
|
||||
:on-before-upload="handleBeforeUpload"
|
||||
>
|
||||
点击上传
|
||||
</n-upload>
|
||||
<n-modal
|
||||
preset="card"
|
||||
style="width: 600px"
|
||||
>
|
||||
<img :src="previewImageUrl" style="width: 100%">
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, reactive, toRefs, computed, unref } from 'vue'
|
||||
import { getToken } from '@/utils/token'
|
||||
export default defineComponent({
|
||||
name: 'ImgUpload',
|
||||
props: {
|
||||
options: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const data = reactive({
|
||||
fileList: []
|
||||
})
|
||||
const BaseURL = window.__APP__GLOB__CONF__?.VITE_APP_GLOB_BASE_API || import.meta.env.VITE_APP_GLOB_BASE_API
|
||||
const getImgOptions = computed(() => {
|
||||
return {
|
||||
...unref(props.options),
|
||||
listType: 'image-card',
|
||||
defaultUpload: props.options.defaultUpload || false,
|
||||
action: `${BaseURL}${props.options.action}`,
|
||||
headers: {
|
||||
'Authorization': getToken(),
|
||||
...props.options.headers
|
||||
}
|
||||
}
|
||||
})
|
||||
function handleChange(res) {
|
||||
console.log('已选择的文件:', res.file.file)
|
||||
}
|
||||
/**
|
||||
* @description: 上传前判断文件是否符合条件
|
||||
* @param {*} options
|
||||
* @return {*}
|
||||
*/
|
||||
function handleBeforeUpload(options) {
|
||||
const { file, fileList } = options
|
||||
if (props.size) {
|
||||
const size = file.file.size
|
||||
if (size < props.size * 1024 * 1024) {
|
||||
fileList.splice(fileList.length - 1)
|
||||
$message.error('选择的文件大小不满足条件!')
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
...toRefs(data),
|
||||
getImgOptions,
|
||||
handleChange,
|
||||
handleBeforeUpload
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
<style scoped lang='scss'>
|
||||
</style>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<VAceEditor
|
||||
ref="editor"
|
||||
v-model:value="showText"
|
||||
style="width:100%;height:150px"
|
||||
lang="json"
|
||||
theme="chrome"
|
||||
@blur="handleInput"
|
||||
@init="initEditor"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
import { reactive, toRefs } from 'vue'
|
||||
import 'ace-builds/src-noconflict/mode-json.js'
|
||||
import 'ace-builds/src-noconflict/theme-chrome.js'
|
||||
import 'ace-builds/src-noconflict/ext-language_tools.js'
|
||||
import { isArray } from '@/utils/is.js'
|
||||
export default {
|
||||
name: 'JsonEditor',
|
||||
components: { VAceEditor },
|
||||
props: {
|
||||
jsonStr: {
|
||||
type: [Array, String],
|
||||
default: () => [{ label: '字段名', value: '字段值' }]
|
||||
}
|
||||
},
|
||||
emits: ['json-change'],
|
||||
setup(props, { emit }) {
|
||||
const data = reactive({
|
||||
options: {
|
||||
tabSize: 2,
|
||||
fontSize: 13
|
||||
},
|
||||
readOnly: false,
|
||||
showText: '',
|
||||
showJson: isArray(props.jsonStr) ? props.jsonStr : JSON.parse(props.jsonStr)
|
||||
})
|
||||
|
||||
const initEditor = () => {
|
||||
data.showText = JSON.stringify(data.showJson, null, 2)
|
||||
}
|
||||
|
||||
function handleInput() {
|
||||
emit('json-change', data.showText)
|
||||
}
|
||||
return {
|
||||
...toRefs(data),
|
||||
initEditor,
|
||||
handleInput
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped lang='scss'>
|
||||
</style>
|
||||
|
|
@ -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,65 @@
|
|||
<template>
|
||||
<div />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
const NMessage = useMessage()
|
||||
|
||||
let loadingMessage = null
|
||||
|
||||
class Message {
|
||||
removeMessage(message, duration = 2000) {
|
||||
setTimeout(() => {
|
||||
if (message) {
|
||||
message.destroy()
|
||||
message = null
|
||||
}
|
||||
}, duration)
|
||||
}
|
||||
|
||||
showMessage(type, content, option = {}) {
|
||||
if (loadingMessage && loadingMessage.type === 'loading') {
|
||||
loadingMessage.type = type
|
||||
loadingMessage.content = content
|
||||
|
||||
if (type !== 'loading') {
|
||||
this.removeMessage(loadingMessage, option.duration)
|
||||
}
|
||||
} else {
|
||||
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,101 @@
|
|||
<template>
|
||||
<n-modal
|
||||
ref="modalRef"
|
||||
v-bind="getModalOptions"
|
||||
:style="`width:${getModalOptions.width}px`"
|
||||
:title="options.title"
|
||||
>
|
||||
<n-card :bordered="false">
|
||||
<slot name="Context" />
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { defineComponent, computed, ref } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'CardDialogModal',
|
||||
props: {
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
emits: {
|
||||
onConfirm: null,
|
||||
onClose: (value) => {
|
||||
return value
|
||||
}
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const modalRef = ref(null)
|
||||
const getModalOptions = computed(() => {
|
||||
return {
|
||||
...props.options,
|
||||
width: props.options.width || 600,
|
||||
preset: props.options.preset || 'dialog',
|
||||
showIcon: !!props.options.showIcon
|
||||
}
|
||||
})
|
||||
const handleConfirm = function() {
|
||||
emit('onConfirm')
|
||||
return false
|
||||
}
|
||||
const handleClose = function() {
|
||||
emit('onClose', true)
|
||||
}
|
||||
|
||||
// setTimeout(() => {
|
||||
// const dialogHeaderEl = document.querySelector('.n-card-header')
|
||||
// const dragDom = document.querySelector('.n-modal')
|
||||
// dragDom.style.overflow = 'auto'
|
||||
// dialogHeaderEl.style.cursor = 'move'
|
||||
// const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null)
|
||||
// const moveDown = (e) => {
|
||||
// // 鼠标按下,计算当前元素距离可视区的距离
|
||||
// const disX = e.clientX - dialogHeaderEl.offsetLeft
|
||||
// const disY = e.clientY - dialogHeaderEl.offsetTop
|
||||
// // 获取到的值带px 正则匹配替换
|
||||
// let styL, styT
|
||||
// // 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
|
||||
// if (sty.left.includes('%')) {
|
||||
// styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100)
|
||||
// styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100)
|
||||
// } else {
|
||||
// styL = +sty.left.replace(/\px/g, '')
|
||||
// styT = +sty.top.replace(/\px/g, '')
|
||||
// }
|
||||
// document.onmousemove = function(e) {
|
||||
// // 计算移动的距离
|
||||
// const l = e.clientX - disX
|
||||
// const t = e.clientY - disY
|
||||
// // 移动当前元素
|
||||
// dragDom.style.left = `${l + styL}px`
|
||||
// dragDom.style.top = `${t + styT}px`
|
||||
// }
|
||||
// document.onmouseup = function(e) {
|
||||
// document.onmousemove = null
|
||||
// document.onmouseup = null
|
||||
// }
|
||||
// }
|
||||
// dialogHeaderEl.onmousedown = moveDown
|
||||
// })
|
||||
|
||||
return {
|
||||
modalRef,
|
||||
getModalOptions,
|
||||
handleConfirm,
|
||||
handleClose
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang='scss'>
|
||||
::v-deep(.n-scrollbar-content){
|
||||
&:first-child{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
<template>
|
||||
<div>
|
||||
<n-form ref="formRef" v-bind="getFormOptions">
|
||||
<template v-for="(item, index) in getFormOptions.info" :key="`${index}-${item.label}`">
|
||||
<n-form-item :class="{'hidden-item': index > showItemNum}" :label="item.label">
|
||||
<n-input v-if="['input'].includes(item.type) || !item.type" v-model:value="getFormOptions.form[item.key]" v-bind="item.props" />
|
||||
<n-select v-if="['select'].includes(item.type)" v-model:value="getFormOptions.form[item.key]" v-bind="item.props" />
|
||||
<AreaCascader v-if="['area'].includes(item.type)" :ref="el=>{itemRefs[item.refIndex] = el}" v-model:value="getFormOptions.form[item.key]" v-bind="item.props" @selectd="handleSelect" />
|
||||
<n-date-picker v-if="['date'].includes(item.type)" v-model:formatted-value="getFormOptions.form[item.key]" v-bind="item.props" />
|
||||
</n-form-item>
|
||||
</template>
|
||||
<n-form-item class="form__button">
|
||||
<n-button type="primary" @click="handleSearch">查询</n-button>
|
||||
<n-button @click="handleReset">重置</n-button>
|
||||
<n-button
|
||||
v-if="showButton"
|
||||
type="text"
|
||||
@click="showMoreItem"
|
||||
>{{ showItemNum === getFormOptions.info.length - 1 ? '收起' : '展开' }}</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, ref, unref, watch } from 'vue'
|
||||
import { NForm } from 'naive-ui'
|
||||
import AreaCascader from '@/components/AreaCascader/index.vue'
|
||||
export default {
|
||||
name: 'SearchPage',
|
||||
components: { AreaCascader },
|
||||
props: {
|
||||
...NForm.props,
|
||||
info: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
emits: ['search', 'change', 'reset'],
|
||||
setup(props, { emit }) {
|
||||
const showItemNum = ref(30)
|
||||
const len = ref(props.info.length - 1)
|
||||
const itemRefs = ref([])
|
||||
const showButton = ref(!!(showItemNum.value < len.value))
|
||||
const form = ref({})
|
||||
/* 初始化搜索表单信息 */
|
||||
function initForm() {
|
||||
Object.keys(form.value).forEach((key) => {
|
||||
const index = unref(props).info.findIndex((item) => item.key === key)
|
||||
form.value[key] = (props).info[index].value || null
|
||||
})
|
||||
itemRefs.value.forEach((item) => {
|
||||
item.clearValue()
|
||||
})
|
||||
}
|
||||
/* 地图选择事件 */
|
||||
function handleSelect(data) {
|
||||
form.value = {
|
||||
...form.value,
|
||||
...data
|
||||
}
|
||||
}
|
||||
const getFormOptions = computed(() => {
|
||||
props.info.forEach((item) => {
|
||||
// const hasInit = item.init || false
|
||||
// const hasKey = Object.keys(form.value).includes(item.key)
|
||||
// form.value[item.key] = hasInit ? (item.value || null) : hasKey ? form.value[item.key] : (item.value || null)
|
||||
form.value[item.key] = item.value || null
|
||||
})
|
||||
return {
|
||||
form: unref(form),
|
||||
labelWidth: 'auto',
|
||||
labelPlacement: 'left',
|
||||
inline: true,
|
||||
info: [...props.info]
|
||||
}
|
||||
})
|
||||
|
||||
watch(getFormOptions.value.form,
|
||||
(value) => {
|
||||
emit('change', getFormOptions.value.form)
|
||||
})
|
||||
|
||||
function handleSearch() {
|
||||
emit('search', getFormOptions.value.form)
|
||||
}
|
||||
function handleReset() {
|
||||
initForm()
|
||||
emit('reset', getFormOptions.value.form)
|
||||
}
|
||||
|
||||
function setFormValue(params) {
|
||||
Object.keys(params).forEach((key) => {
|
||||
const index = unref(props).info.findIndex((item) => item.key === key)
|
||||
form.value[key] = (props).info[index].value || null
|
||||
})
|
||||
}
|
||||
|
||||
function showMoreItem() {
|
||||
showItemNum.value = showItemNum.value === len.value ? 3 : len.value
|
||||
}
|
||||
return {
|
||||
showItemNum,
|
||||
showButton,
|
||||
getFormOptions,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
setFormValue,
|
||||
handleSelect,
|
||||
itemRefs,
|
||||
showMoreItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped lang='scss'>
|
||||
.n-form{
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.n-form-item{
|
||||
.n-input{
|
||||
width: 200px;
|
||||
}
|
||||
.n-select{
|
||||
width: 200px;
|
||||
}
|
||||
.n-cascader{
|
||||
width: 200px;
|
||||
}
|
||||
&.hidden-item{
|
||||
display: none;
|
||||
}
|
||||
transition: all 10s;
|
||||
}
|
||||
.form__button{
|
||||
button+button{
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<div v-if="external" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-bind="$attrs" />
|
||||
<svg v-else :class="svgClass" aria-hidden="true" v-bind="$attrs">
|
||||
<use :xlink:href="iconName" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isExternal } from '@/utils/is.js'
|
||||
import { defineComponent, computed } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'SvgIcon',
|
||||
props: {
|
||||
prefix: {
|
||||
type: String,
|
||||
default: 'icon'
|
||||
},
|
||||
iconClass: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const external = computed(() => {
|
||||
return isExternal(props.iconClass)
|
||||
})
|
||||
|
||||
const iconName = computed(() => {
|
||||
return `#icon-${props.iconClass}`
|
||||
})
|
||||
|
||||
const svgClass = computed(() => {
|
||||
if (props.className) {
|
||||
return 'svg-icon ' + props.className
|
||||
} else {
|
||||
return 'svg-icon'
|
||||
}
|
||||
})
|
||||
const styleExternalIcon = computed(() => {
|
||||
return {
|
||||
mask: `url(${props.iconClass}) no-repeat 50% 50%`,
|
||||
'-webkit-mask': `url(${props.iconClass}) no-repeat 50% 50%`
|
||||
}
|
||||
})
|
||||
return {
|
||||
external,
|
||||
iconName,
|
||||
svgClass,
|
||||
styleExternalIcon
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.svg-external-icon {
|
||||
background-color: currentColor;
|
||||
mask-size: cover!important;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
<template>
|
||||
<div class="tinymce-box">
|
||||
<Editor
|
||||
id="myedit"
|
||||
v-model="content"
|
||||
tag-name="div"
|
||||
:init="init"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Editor from '@tinymce/tinymce-vue'
|
||||
|
||||
// 引入方式引入node_modules里的tinymce相关文件文件
|
||||
import tinymce from 'tinymce/tinymce' // tinymce默认hidden,不引入则不显示编辑器
|
||||
import 'tinymce/themes/silver' // 编辑器主题,不引入则报错
|
||||
import 'tinymce/icons/default' // 引入编辑器图标icon,不引入则不显示对应图标
|
||||
|
||||
// 引入编辑器插件
|
||||
import 'tinymce/plugins/advlist' // 高级列表
|
||||
import 'tinymce/plugins/anchor' // 锚点
|
||||
import 'tinymce/plugins/autolink' // 自动链接
|
||||
import 'tinymce/plugins/autoresize' // 编辑器高度自适应,注:plugins里引入此插件时,Init里设置的height将失效
|
||||
import 'tinymce/plugins/autosave' // 自动存稿
|
||||
import 'tinymce/plugins/charmap' // 特殊字符
|
||||
import 'tinymce/plugins/code' // 编辑源码
|
||||
import 'tinymce/plugins/codesample' // 代码示例
|
||||
import 'tinymce/plugins/directionality' // 文字方向
|
||||
import 'tinymce/plugins/emoticons' // 表情
|
||||
import 'tinymce/plugins/fullpage' // 文档属性
|
||||
import 'tinymce/plugins/fullscreen' // 全屏
|
||||
import 'tinymce/plugins/help' // 帮助
|
||||
import 'tinymce/plugins/hr' // 水平分割线
|
||||
import 'tinymce/plugins/image' // 插入编辑图片
|
||||
import 'tinymce/plugins/importcss' // 引入css
|
||||
import 'tinymce/plugins/insertdatetime' // 插入日期时间
|
||||
import 'tinymce/plugins/link' // 超链接
|
||||
import 'tinymce/plugins/lists' // 列表插件
|
||||
import 'tinymce/plugins/media' // 插入编辑媒体
|
||||
import 'tinymce/plugins/nonbreaking' // 插入不间断空格
|
||||
import 'tinymce/plugins/pagebreak' // 插入分页符
|
||||
import 'tinymce/plugins/paste' // 粘贴插件
|
||||
import 'tinymce/plugins/preview' // 预览
|
||||
import 'tinymce/plugins/print' // 打印
|
||||
import 'tinymce/plugins/quickbars' // 快速工具栏
|
||||
import 'tinymce/plugins/save' // 保存
|
||||
import 'tinymce/plugins/searchreplace' // 查找替换
|
||||
import 'tinymce/plugins/tabfocus' // 切入切出,按tab键切出编辑器,切入页面其他输入框中
|
||||
import 'tinymce/plugins/table' // 表格
|
||||
import 'tinymce/plugins/template' // 内容模板
|
||||
import 'tinymce/plugins/textcolor' // 文字颜色
|
||||
import 'tinymce/plugins/textpattern' // 快速排版
|
||||
import 'tinymce/plugins/toc' // 目录生成器
|
||||
import 'tinymce/plugins/visualblocks' // 显示元素范围
|
||||
import 'tinymce/plugins/visualchars' // 显示不可见字符
|
||||
import 'tinymce/plugins/wordcount' // 字数统计
|
||||
import { ref, watch } from 'vue'
|
||||
export default {
|
||||
name: 'TinymceEditor',
|
||||
components: {
|
||||
Editor
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
plugins: {
|
||||
type: [String, Array],
|
||||
default:
|
||||
`print preview searchreplace autolink directionality visualblocks visualchars
|
||||
fullscreen image link media template code codesample table charmap hr pagebreak
|
||||
nonbreaking anchor insertdatetime advlist lists wordcount textpattern autosave`
|
||||
},
|
||||
toolbar: {
|
||||
type: [String, Array],
|
||||
default:
|
||||
`fullscreen undo redo restoredraft | cut copy paste pastetext | forecolor backcolor bold italic underline strikethrough link anchor |
|
||||
alignleft aligncenter alignright alignjustify outdent indent | styleselect formatselect fontselect fontsizeselect | bullist numlist |
|
||||
blockquote subscript superscript removeformat | table image media charmap emoticons hr pagebreak insertdatetime print preview |
|
||||
code selectall | indent2em lineheight formatpainter axupimgs`
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 600
|
||||
}
|
||||
},
|
||||
emits: { 'update:modelValue': null },
|
||||
setup(props, { emit }) {
|
||||
const init = {
|
||||
language_url: '/tinymce/langs/zh_CN.js',
|
||||
language: 'zh_CN',
|
||||
skin_url: '/tinymce/skins/ui/oxide', // 浅色
|
||||
// skin_url: '/tinymce/skins/ui/oxide-dark',//暗色
|
||||
content_css: '/tinymce/skins/content/default/content.css',
|
||||
plugins: props.plugins, // 插件配置
|
||||
toolbar: props.toolbar, // 工具栏配置,设为false则隐藏
|
||||
toolbar_mode: 'sliding',
|
||||
menubar: 'file edit insert view format table tools', // 菜单栏配置,设为false则隐藏,不配置则默认显示全部菜单,也可自定义配置--查看 http://tinymce.ax-z.cn/configure/editor-appearance.php --搜索“自定义菜单”
|
||||
menu: {
|
||||
// file: { title: '文件', items: 'newdocument' },
|
||||
// edit: { title: '编辑', items: 'undo redo | cut copy paste pastetext | selectall' },
|
||||
// insert: { title: '插入', items: 'link image | hr' },
|
||||
// view: { title: '查看', items: 'visualaid' }
|
||||
// format: {
|
||||
// title: '格式',
|
||||
// items:
|
||||
// 'bold italic underline strikethrough superscript subscript | formats | removeformat',
|
||||
// },
|
||||
// table: { title: '表格', items: 'inserttable tableprops deletetable | cell row column' },
|
||||
// tools: { title: '工具', items: 'spellchecker code' },
|
||||
},
|
||||
fontsize_formats: '12px 14px 16px 18px 20px 22px 24px 28px 32px 36px 48px 56px 72px', // 字体大小
|
||||
font_formats:
|
||||
`微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;
|
||||
宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;
|
||||
Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;`,
|
||||
height: props.height, // 注:引入autoresize插件时,此属性失效
|
||||
placeholder: '在这里输入文字',
|
||||
branding: false, // tiny技术支持信息是否显示
|
||||
resize: false, // 编辑器宽高是否可变,false-否,true-高可变,'both'-宽高均可,注意引号
|
||||
statusbar: false, // 最下方的元素路径和字数统计那一栏是否显示
|
||||
elementpath: false, // 元素路径是否显示
|
||||
|
||||
content_style: 'img {max-width:100%;}' // 直接自定义可编辑区域的css样式
|
||||
// content_css: '/tinycontent.css', //以css文件方式自定义可编辑区域的css样式,css文件需自己创建并引入
|
||||
}
|
||||
/* 初始化编辑器 */
|
||||
tinymce.init
|
||||
|
||||
const setEditMode = type => {
|
||||
tinymce.editors['myedit'].setMode(type) // 开启只读模式
|
||||
}
|
||||
const content = ref(props.modelValue)
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
console.log(value)
|
||||
content.value = value
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => content.value,
|
||||
(value) => {
|
||||
emit('update:modelValue', content.value)
|
||||
}
|
||||
)
|
||||
return {
|
||||
content,
|
||||
init,
|
||||
setEditMode
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.tox,.tox-tinymce-aux{
|
||||
z-index: 3000 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
<template>
|
||||
<div v-if="isFailed && useEmpty">
|
||||
<slot name="empty" />
|
||||
</div>
|
||||
<div v-else :id="getPlayerId" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { defineComponent, reactive, ref, computed, toRefs, nextTick } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'VideoPlayer',
|
||||
props: {
|
||||
/* 播放器id */
|
||||
id: {
|
||||
type: String,
|
||||
default: () => 'palyer'
|
||||
},
|
||||
/* 是有填充空白 */
|
||||
useEmpty: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['timeUpdate', 'video-status'],
|
||||
setup(props, { emit }) {
|
||||
const videoPlayer = Symbol.for(props.id)
|
||||
const data = reactive({
|
||||
[videoPlayer]: null,
|
||||
seekTime: null,
|
||||
canSeek: true,
|
||||
isSeek: false,
|
||||
isFailed: true
|
||||
})
|
||||
/* 获取播放器的id */
|
||||
const getPlayerId = computed(() => {
|
||||
return props.id
|
||||
})
|
||||
|
||||
/* 播放器组件 */
|
||||
const toolComponent = Aliplayer.Component({
|
||||
/* 时间更新事件 */
|
||||
timeupdate(player, e) {
|
||||
data.seekTime = player.getCurrentTime()
|
||||
emit('video-status', data.isSeek ? 'skip' : 'playing')
|
||||
},
|
||||
ready(player, e) {
|
||||
emit('video-status', 'ready')
|
||||
},
|
||||
pause(player, e) {
|
||||
emit('video-status', 'pause')
|
||||
},
|
||||
play(player, e) {
|
||||
emit('video-status', 'play')
|
||||
},
|
||||
ended(player, e) {
|
||||
emit('video-status', 'ended')
|
||||
},
|
||||
error(player, e) {
|
||||
data.isFailed = true
|
||||
emit('video-status', 'error')
|
||||
}
|
||||
})
|
||||
|
||||
/* 初始化播放器 */
|
||||
function init(options) {
|
||||
if (!options.source) {
|
||||
data.isFailed = true
|
||||
} else {
|
||||
data.isFailed = false
|
||||
nextTick(() => {
|
||||
/* 实例化ali播放器 */
|
||||
const player = new Aliplayer(
|
||||
{
|
||||
id: props.id,
|
||||
width: '500px',
|
||||
height: '260px',
|
||||
autoplay: true,
|
||||
...options,
|
||||
components: [toolComponent]
|
||||
},
|
||||
function(player) { player.mute() }
|
||||
)
|
||||
/* 监听开始拖拽事件 */
|
||||
player.on('startSeek', ({ paramData }) => {
|
||||
/* 仅变更标识 */
|
||||
data.isSeek = true
|
||||
})
|
||||
/* 监听完成拖拽事件 */
|
||||
player.on('completeSeek', ({ paramData }) => {
|
||||
/* 仅变更标识 */
|
||||
data.isSeek = false
|
||||
data.seekTime = paramData
|
||||
/* 是否通知跳转 */
|
||||
if (data.canSeek) {
|
||||
emit('video-status', 'skip')
|
||||
}
|
||||
})
|
||||
data[videoPlayer] = player
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* 获取当前播放器的时间 */
|
||||
function getTime() {
|
||||
let currentTime = 0
|
||||
let duration = 0
|
||||
const seekTime = data.seekTime
|
||||
if (data[videoPlayer] && !data.isFailed) {
|
||||
currentTime = data[videoPlayer]?.getCurrentTime()
|
||||
duration = data[videoPlayer]?.getDuration()
|
||||
}
|
||||
return {
|
||||
currentTime,
|
||||
duration,
|
||||
seekTime
|
||||
}
|
||||
}
|
||||
/* 设定播放器播放时间 */
|
||||
function seekTime(time) {
|
||||
if (data[videoPlayer] && !data.isFailed) {
|
||||
data.canSeek = false
|
||||
data[videoPlayer]?.seek(time)
|
||||
setTimeout(() => {
|
||||
data.canSeek = true
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
/* 设定播放器开始 */
|
||||
function playVideo() {
|
||||
if (data[videoPlayer] && !data.isFailed) {
|
||||
data[videoPlayer]?.play()
|
||||
}
|
||||
}
|
||||
/* 设定播放器暂停 */
|
||||
function pauseVideo() {
|
||||
if (data[videoPlayer] && !data.isFailed) {
|
||||
data[videoPlayer]?.pause()
|
||||
}
|
||||
}
|
||||
/* 销毁播放器 */
|
||||
function disposeVideo() {
|
||||
if (data[videoPlayer] && !data.isFailed) {
|
||||
data[videoPlayer]?.dispose()
|
||||
}
|
||||
data.isFailed = true
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(data),
|
||||
getPlayerId,
|
||||
init,
|
||||
getTime,
|
||||
seekTime,
|
||||
playVideo,
|
||||
pauseVideo,
|
||||
disposeVideo
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.prism-player .prism-ErrorMessage .prism-error-operation{
|
||||
border-bottom: none;
|
||||
}
|
||||
.prism-player .prism-ErrorMessage .prism-error-operation a.prism-button.prism-button-refresh{
|
||||
display: none;
|
||||
}
|
||||
.prism-player .prism-ErrorMessage .prism-error-operation a.prism-button.prism-button-orange{
|
||||
display: none;
|
||||
}
|
||||
/* .prism-player .prism-ErrorMessage .prism-detect-info.prism-center{
|
||||
display: none;
|
||||
} */
|
||||
</style>
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
<template>
|
||||
<n-layout-header class="layout__header" bordered>
|
||||
<!-- <div class="header__logo">
|
||||
<n-image height="18" src="/logo.png" preview-disabled />
|
||||
</div> -->
|
||||
<!-- <n-dropdown trigger="hover" :options="options" @select="handleSelect">
|
||||
<div class="user_msg">
|
||||
<n-image
|
||||
class="user_avatar"
|
||||
:src="userInfo.avatar"
|
||||
preview-disabled
|
||||
/>
|
||||
<span class="user_name">{{ userInfo.realname }}</span>
|
||||
</div>
|
||||
</n-dropdown> -->
|
||||
</n-layout-header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, reactive, toRefs, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { EditOutlined, LogoutOutlined } from '@vicons/antd'
|
||||
import { renderIcon } from '@/utils'
|
||||
import { useUserStore } from '@/store/modules/user.js'
|
||||
import { useSettingStore } from '@/store/modules/setting.js'
|
||||
export default defineComponent({
|
||||
name: 'LayoutHeader',
|
||||
setup() {
|
||||
const router = useRouter()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const settingStore = useSettingStore()
|
||||
const data = reactive({
|
||||
options: [
|
||||
{
|
||||
label: '修改密码',
|
||||
key: 'edit',
|
||||
icon: renderIcon(EditOutlined)
|
||||
},
|
||||
{
|
||||
label: '退出登录',
|
||||
key: 'out',
|
||||
icon: renderIcon(LogoutOutlined)
|
||||
}
|
||||
],
|
||||
userInfo: {
|
||||
avatar: '',
|
||||
realname: '管理员'
|
||||
}
|
||||
})
|
||||
|
||||
const getLogoWidth = computed(() => {
|
||||
return settingStore.getSidebarSetting.width - 30
|
||||
})
|
||||
|
||||
async function handleSelect(key) {
|
||||
switch (key) {
|
||||
case 'out':
|
||||
await logOut()
|
||||
}
|
||||
}
|
||||
|
||||
async function logOut() {
|
||||
const res = await userStore.userLogout()
|
||||
if (res.code === 0) {
|
||||
router.replace('/login')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(data),
|
||||
getLogoWidth,
|
||||
handleSelect
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout__header {
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.header__logo{
|
||||
height: 18px;
|
||||
}
|
||||
.user_msg {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
.user_avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
<template>
|
||||
<Modal
|
||||
:options="getModalOptions"
|
||||
:on-positive-click="handleConfirm"
|
||||
:on-negative-click="handleClose"
|
||||
:on-close="handleClose"
|
||||
>
|
||||
<template #Context>
|
||||
<n-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
label-placement="left"
|
||||
:rules="rules"
|
||||
:on-positive-click="handleConfirm"
|
||||
:on-negative-click="handleClose"
|
||||
>
|
||||
<n-form-item
|
||||
label="旧密码:"
|
||||
path="oldPassword"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="form.oldPassword"
|
||||
type="password"
|
||||
:maxlength="20"
|
||||
placeholder="请输入旧密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
label="新密码:"
|
||||
path="newPassword"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="form.newPassword"
|
||||
type="password"
|
||||
:maxlength="20"
|
||||
placeholder="请输入新密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
label="确认密码:"
|
||||
path="configmPassword"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="form.configmPassword"
|
||||
type="password"
|
||||
:maxlength="20"
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
<script>
|
||||
import Modal from '../../../../components/Modal/index.vue'
|
||||
import { updatePwd } from '@/api/home/index.js'
|
||||
import { reactive, toRefs, computed } from '@vue/reactivity'
|
||||
export default {
|
||||
name: 'UpdateModal',
|
||||
components: { Modal },
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: {
|
||||
'update:visible': null
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const data = reactive({
|
||||
form: {
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
configmPassword: ''
|
||||
},
|
||||
rules: {
|
||||
oldPassword: {
|
||||
required: true,
|
||||
trigger: ['blur', 'input'],
|
||||
message: '请输入旧密码'
|
||||
},
|
||||
newPassword: {
|
||||
required: true,
|
||||
trigger: ['blur', 'input'],
|
||||
message: '请输入新密码'
|
||||
},
|
||||
configmPassword: {
|
||||
required: true,
|
||||
trigger: ['blur', 'input'],
|
||||
message: '请再次输入新密码'
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
const getModalOptions = computed(() => {
|
||||
return {
|
||||
title: '修改密码',
|
||||
show: props.visible,
|
||||
negativeText: '取消',
|
||||
positiveText: '确认'
|
||||
}
|
||||
})
|
||||
|
||||
/* 关闭弹窗 */
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
return {
|
||||
...toRefs(data),
|
||||
getModalOptions,
|
||||
handleClose
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleConfirm() {
|
||||
this.$refs.formRef.validate((errors) => {
|
||||
if (!errors) {
|
||||
updatePwd(this.form).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.handleClose()
|
||||
$message.success(res.msg)
|
||||
} else {
|
||||
$message.error(res.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
<template>
|
||||
<n-dropdown trigger="hover" :options="options" @select="selectKey">
|
||||
<div class="user_msg">
|
||||
<n-image
|
||||
class="user_avatar"
|
||||
:src="userInfo.avatar"
|
||||
preview-disabled
|
||||
/>
|
||||
<span class="user_name">{{ userInfo.realname }}</span>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
|
||||
<!-- 修改密码 -->
|
||||
<update-modal v-if="modalShow" v-modal:visible="modalShow" />
|
||||
</template>
|
||||
<script>
|
||||
import { reactive, toRefs, h } from 'vue'
|
||||
import { NIcon } from 'naive-ui'
|
||||
import {
|
||||
Pencil as EditIcon,
|
||||
LogOutOutline as LogoutIcon
|
||||
} from '@vicons/ionicons5'
|
||||
import { useDialog } from 'naive-ui'
|
||||
import { loginOut } from '@/api/login/index.js'
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { useUserStore } from '@/store/modules/user.js'
|
||||
import UpdateModal from './components/UpdateModal.vue'
|
||||
export default {
|
||||
name: 'LogOut',
|
||||
components: { UpdateModal },
|
||||
setup() {
|
||||
const renderIcon = (icon) => {
|
||||
return () => {
|
||||
return h(NIcon, null, {
|
||||
default: () => h(icon)
|
||||
})
|
||||
}
|
||||
}
|
||||
const data = reactive({
|
||||
options: [
|
||||
{
|
||||
label: '修改密码',
|
||||
key: 'edit',
|
||||
icon: renderIcon(EditIcon)
|
||||
},
|
||||
{
|
||||
label: '退出登录',
|
||||
key: 'out',
|
||||
icon: renderIcon(LogoutIcon)
|
||||
}
|
||||
],
|
||||
modalShow: false
|
||||
})
|
||||
|
||||
// 选中选项触发的回调
|
||||
const dialog = useDialog()
|
||||
|
||||
return {
|
||||
...toRefs(data),
|
||||
dialog
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(useUserStore, {
|
||||
userInfo: 'userInfo'
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useUserStore, ['logout']),
|
||||
selectKey(key) {
|
||||
console.log(key)
|
||||
if (key === 'out') {
|
||||
this.dialog.warning({
|
||||
title: '提示',
|
||||
content: '确定要退出登录吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
loginOut().then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$router.replace('/login')
|
||||
this.logout()
|
||||
$message.success(res.msg)
|
||||
} else {
|
||||
$message.error(res.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (key === 'edit') {
|
||||
this.modalShow = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.user_msg {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
.user_avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<n-menu
|
||||
:mode="menuMode"
|
||||
:value="(currentRoute.title && currentRoute.meta.activeMenu) || currentRoute.title"
|
||||
: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) {
|
||||
// console.log('routes', routes)
|
||||
const options = []
|
||||
routes.forEach((route) => {
|
||||
if (route.title && !route.isHidden) {
|
||||
const curOption = {
|
||||
label: (route.meta && route.meta.title) || route.title,
|
||||
key: route.title,
|
||||
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,20 @@
|
|||
<template>
|
||||
<n-layout-sider
|
||||
class="layout_sidebar"
|
||||
v-bind="getSideOptions"
|
||||
>
|
||||
<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,468 @@
|
|||
<template>
|
||||
<div class="tabs-view" :class="{'tabs-view-fix': tagsMenuSetting.fixed,}" :style="getChangeStyle">
|
||||
<div class="tabs-view-main">
|
||||
<div ref="navWrap" class="tabs-card" :class="{ 'tabs-card-scrollable': state.scrollable }">
|
||||
<span class="tabs-card-prev" :class="{ 'tabs-card-prev-hide': !state.scrollable }" @click="scrollPrev">
|
||||
<n-icon size="16" color="#515a6e">
|
||||
<LeftOutlined />
|
||||
</n-icon>
|
||||
</span>
|
||||
<span class="tabs-card-next" :class="{ 'tabs-card-next-hide': !state.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': state.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 class="tabs-close">
|
||||
<n-dropdown
|
||||
trigger="hover"
|
||||
placement="bottom-end"
|
||||
:options="tagsMemuOptions"
|
||||
@select="closeHandleSelect"
|
||||
>
|
||||
<div class="tabs-close-btn">
|
||||
<n-icon size="16" color="#515a6e">
|
||||
<DownOutlined />
|
||||
</n-icon>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
|
||||
<n-dropdown
|
||||
:show="state.showDropdown"
|
||||
:x="state.dropdownX"
|
||||
:y="state.dropdownY"
|
||||
placement="bottom-start"
|
||||
:options="tagsMemuOptions"
|
||||
@clickoutside="onClickOutside"
|
||||
@select="closeHandleSelect"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
CloseOutlined,
|
||||
DownOutlined,
|
||||
ReloadOutlined,
|
||||
ColumnWidthOutlined,
|
||||
MinusOutlined
|
||||
} from '@vicons/antd'
|
||||
import Draggable from 'vuedraggable'
|
||||
import { reactive, computed, watch, defineProps, ref, unref, provide, nextTick } 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'
|
||||
import { isString } from '@/utils/is.js'
|
||||
import { renderIcon } from '@/utils'
|
||||
|
||||
const props = defineProps({
|
||||
collapsed: {
|
||||
type: Boolean
|
||||
},
|
||||
menuMode: {
|
||||
type: String,
|
||||
default: 'sidebar'
|
||||
}
|
||||
})
|
||||
|
||||
/* 白名单 */
|
||||
const whiteList = ['Login', 'Redirect', 'NOT_FOUND']
|
||||
/* 基础页面 */
|
||||
const baseMenu = '/home'
|
||||
|
||||
/* 获取路由器 */
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { push, replace } = router
|
||||
/* 使用store数据 */
|
||||
const tagsMenuStore = useTagsMenuStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const tabsList = computed(() => tagsMenuStore.tabsList)
|
||||
const tagsMenuSetting = computed(() => settingStore.getTagsMenuSetting)
|
||||
|
||||
/* 组装样式 */
|
||||
const getChangeStyle = computed(() => {
|
||||
const { collapsed, menuMode } = props
|
||||
const { cWidth, width } = unref(settingStore.getSidebarSetting)
|
||||
const { fixed } = unref(settingStore.getTagsMenuSetting)
|
||||
const lenNum = menuMode === 'header' ? '0px' : collapsed ? `${cWidth}px` : `${width}px`
|
||||
return {
|
||||
left: lenNum,
|
||||
width: `calc(100% - ${!fixed ? '0px' : lenNum})`
|
||||
}
|
||||
})
|
||||
|
||||
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,
|
||||
showDropdown: false,
|
||||
dropdownX: 0,
|
||||
dropdownY: 0
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
(to) => {
|
||||
// if (whiteList.includes(route.name)) return
|
||||
state.activeKey = to
|
||||
tagsMenuStore.addTabs(getSimpleRoute(route))
|
||||
// updateNavScroll(true)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
/**
|
||||
* @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 {*}
|
||||
*/
|
||||
function closeTabItem(e) {
|
||||
const { fullPath } = e
|
||||
const routeInfo = tabsList.value.find((item) => item.fullPath === fullPath)
|
||||
removeTab(routeInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description:
|
||||
* @return {*}
|
||||
*/
|
||||
|
||||
const removeTab = (route) => {
|
||||
if (tabsList.value.length === 1) {
|
||||
// return message.warning('这已经是最后一页,不能再关闭了!')
|
||||
}
|
||||
// delKeepAliveCompName()
|
||||
tagsMenuStore.closeCurrentTab(route)
|
||||
// 如果关闭的是当前页
|
||||
if (state.activeKey === route.fullPath) {
|
||||
const currentRoute = tabsList.value[Math.max(0, tabsList.value.length - 1)]
|
||||
state.activeKey = currentRoute.fullPath
|
||||
router.push(currentRoute)
|
||||
}
|
||||
// updateNavScroll()
|
||||
}
|
||||
|
||||
// const delKeepAliveCompName = () => {
|
||||
// if (route.meta.keepAlive) {
|
||||
// const name = router.currentRoute.value.matched.find((item) => item.name === route.name)
|
||||
// ?.components?.default.name
|
||||
// if (name) {
|
||||
// asyncRouteStore.keepAliveComponents = asyncRouteStore.keepAliveComponents.filter(
|
||||
// (item) => item != name
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/* dropdown--start */
|
||||
/* 右侧下拉菜单 */
|
||||
const isCurrent = ref(false)
|
||||
const tagsMemuOptions = computed(() => {
|
||||
const isDisabled = unref(tabsList).length <= 1
|
||||
return [
|
||||
{
|
||||
label: '刷新当前',
|
||||
key: '1',
|
||||
icon: renderIcon(ReloadOutlined)
|
||||
},
|
||||
{
|
||||
label: `关闭当前`,
|
||||
key: '2',
|
||||
disabled: unref(isCurrent) || isDisabled,
|
||||
icon: renderIcon(CloseOutlined)
|
||||
},
|
||||
{
|
||||
label: '关闭其他',
|
||||
key: '3',
|
||||
disabled: isDisabled,
|
||||
icon: renderIcon(ColumnWidthOutlined)
|
||||
},
|
||||
{
|
||||
label: '关闭全部',
|
||||
key: '4',
|
||||
disabled: isDisabled,
|
||||
icon: renderIcon(MinusOutlined)
|
||||
}
|
||||
]
|
||||
})
|
||||
/* 操作右侧下拉菜单 */
|
||||
const closeHandleSelect = (key) => {
|
||||
switch (key) {
|
||||
// 刷新
|
||||
case '1':
|
||||
reloadPage()
|
||||
break
|
||||
// 关闭
|
||||
case '2':
|
||||
removeTab(route)
|
||||
break
|
||||
// 关闭其他
|
||||
case '3':
|
||||
closeOther(route)
|
||||
break
|
||||
// 关闭所有
|
||||
case '4':
|
||||
closeAll()
|
||||
break
|
||||
}
|
||||
// updateNavScroll()
|
||||
state.showDropdown = false
|
||||
}
|
||||
/* 刷新页面 */
|
||||
const reloadPage = () => {
|
||||
// delKeepAliveCompName()
|
||||
router.push({
|
||||
path: '/redirect' + unref(route).fullPath
|
||||
})
|
||||
}
|
||||
/* 注入刷新页面方法 */
|
||||
provide('reloadPage', reloadPage)
|
||||
/* 关闭其他 */
|
||||
const closeOther = (route) => {
|
||||
tagsMenuStore.closeOtherTabs(route)
|
||||
state.activeKey = route.fullPath
|
||||
router.replace(route.fullPath)
|
||||
// updateNavScroll()
|
||||
}
|
||||
|
||||
/* 关闭全部 */
|
||||
const closeAll = () => {
|
||||
tagsMenuStore.closeAllTabs()
|
||||
router.replace(baseMenu)
|
||||
// updateNavScroll()
|
||||
}
|
||||
/* dropdown--end */
|
||||
/* contextMenu--start */
|
||||
/**
|
||||
* @description: 右键菜单
|
||||
* @return {*}
|
||||
*/
|
||||
function handleContextMenu(e, item) {
|
||||
e.preventDefault()
|
||||
isCurrent.value = baseMenu === item.path
|
||||
state.showDropdown = false
|
||||
nextTick().then(() => {
|
||||
state.showDropdown = true
|
||||
state.dropdownX = e.clientX
|
||||
state.dropdownY = e.clientY
|
||||
})
|
||||
}
|
||||
function onClickOutside() {
|
||||
state.showDropdown = false
|
||||
}
|
||||
/* contextMenu--end */
|
||||
/**
|
||||
* @description: 页面跳转
|
||||
* @param undefined
|
||||
* @param undefined
|
||||
* @return {*}
|
||||
*/
|
||||
function go(opt, isReplace = false) {
|
||||
if (!opt) {
|
||||
return
|
||||
}
|
||||
if (isString(opt)) {
|
||||
isReplace ? replace(opt).catch(e => console.log(e)) : push(opt).catch(e => console.log(e))
|
||||
} else {
|
||||
const o = opt
|
||||
isReplace ? replace(o).catch(e => console.log(e)) : push(o).catch(e => console.log(e))
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped lang='scss'>
|
||||
.tabs-view {
|
||||
width: 100%;
|
||||
padding: 6px 0;
|
||||
display: flex;
|
||||
transition: all 0.2s ease-in-out;
|
||||
background: #f5f7f9;
|
||||
.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;
|
||||
.tabs-card-scroll-item {
|
||||
background: #ffffff;
|
||||
// color: v-bind('tagsMenuSetting.color');
|
||||
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: #2d8cf0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-close {
|
||||
min-width: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
// background: var(--color);
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
.tabs-close-btn {
|
||||
// color: var(--color);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-view-fix {
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
padding: 6px 19px 6px 10px;
|
||||
left: 200px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
<template>
|
||||
<Modal
|
||||
:options="getModalOptions"
|
||||
:on-positive-click="handleConfirm"
|
||||
:on-negative-click="handleClose"
|
||||
:on-close="handleClose"
|
||||
>
|
||||
<template #Context>
|
||||
<n-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
label-placement="left"
|
||||
:rules="rules"
|
||||
:on-positive-click="handleConfirm"
|
||||
:on-negative-click="handleClose"
|
||||
>
|
||||
<n-form-item
|
||||
label="旧密码:"
|
||||
path="oldPassword"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="form.oldPassword"
|
||||
type="password"
|
||||
:maxlength="20"
|
||||
placeholder="请输入旧密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
label="新密码:"
|
||||
path="newPassword"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="form.newPassword"
|
||||
type="password"
|
||||
:maxlength="20"
|
||||
placeholder="请输入新密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
label="确认密码:"
|
||||
path="configmPassword"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="form.configmPassword"
|
||||
type="password"
|
||||
:maxlength="20"
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
<script>
|
||||
import Modal from '@/components/Modal/index.vue'
|
||||
import { updatePwd } from '@/api/home/index.js'
|
||||
import { reactive, toRefs } from '@vue/reactivity'
|
||||
export default {
|
||||
name: 'UpdateModal',
|
||||
components: { Modal },
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: {
|
||||
'update:visible': null
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const data = reactive({
|
||||
form: {
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
configmPassword: ''
|
||||
},
|
||||
rules: {
|
||||
oldPassword: {
|
||||
required: true,
|
||||
trigger: ['blur', 'input'],
|
||||
message: '请输入旧密码'
|
||||
},
|
||||
newPassword: {
|
||||
required: true,
|
||||
trigger: ['blur', 'input'],
|
||||
message: '请输入新密码'
|
||||
},
|
||||
configmPassword: {
|
||||
required: true,
|
||||
trigger: ['blur', 'input'],
|
||||
message: '请再次输入新密码'
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
/* 关闭弹窗 */
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
return {
|
||||
...toRefs(data),
|
||||
handleClose
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleConfirm() {
|
||||
this.$refs.formRef.validate((errors) => {
|
||||
if (!errors) {
|
||||
updatePwd(this.form).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.handleClose()
|
||||
$message.success(res.msg)
|
||||
} else {
|
||||
$message.error(res.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<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" :class="{'layout__content--fix': tagsMenuSetting.fixed }">
|
||||
<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'
|
||||
import { useUserStore } from '@/store/modules/user.js'
|
||||
const settingStore = useSettingStore()
|
||||
const menuMode = computed(() => settingStore.getMenuMode)
|
||||
const tagsMenuSetting = computed(() => settingStore.getTagsMenuSetting)
|
||||
const useUser = useUserStore()
|
||||
function getUserNow() {
|
||||
useUser.getUserInfo()
|
||||
}
|
||||
getUserNow()
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout__content--fix{
|
||||
padding-top: 44px;
|
||||
}
|
||||
</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,47 @@
|
|||
import { useUserStore } from '@/store/modules/user'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import { NOT_FOUND_ROUTE, REDIRECT_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()
|
||||
const token = true
|
||||
if (token) {
|
||||
if (to.path === '/login') {
|
||||
next({ path: '/' })
|
||||
} else {
|
||||
// const hasRoutes = !!permissionStore.permissionRoutes.length
|
||||
/* 暂无权限和菜单 */
|
||||
const hasRoutes = true
|
||||
if (hasRoutes) {
|
||||
next()
|
||||
} else {
|
||||
try {
|
||||
// await userStore.getUserInfo()
|
||||
const routes = await permissionStore.generateRoutes()
|
||||
routes.forEach((item) => {
|
||||
router.addRoute(item)
|
||||
})
|
||||
router.addRoute(NOT_FOUND_ROUTE)
|
||||
router.addRoute(REDIRECT_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,72 @@
|
|||
import Layout from '@/layout/index.vue'
|
||||
import Home from '@/views/dashboard/index.vue'
|
||||
import System from './modules/system.js'
|
||||
|
||||
export const basicRoutes = [
|
||||
{
|
||||
path: '/login',
|
||||
title: 'Login',
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
isHidden: true,
|
||||
meta: {
|
||||
title: '登录页'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
title: '控制台',
|
||||
component: Layout,
|
||||
redirect: '/home',
|
||||
meta: {
|
||||
title: '控制台'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'home',
|
||||
title: 'Home',
|
||||
component: Home,
|
||||
meta: {
|
||||
title: '首页',
|
||||
affix: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
...System
|
||||
]
|
||||
|
||||
export const NOT_FOUND_ROUTE = {
|
||||
title: 'NOT_FOUND',
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/404',
|
||||
isHidden: true
|
||||
}
|
||||
|
||||
export const REDIRECT_ROUTE = {
|
||||
path: '/redirect',
|
||||
title: 'Redirect',
|
||||
component: Layout,
|
||||
meta: {
|
||||
title: 'Redirect',
|
||||
hideBreadcrumb: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/redirect/:path(.*)',
|
||||
name: 'Redirect',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
title: 'Redirect',
|
||||
hideBreadcrumb: 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,5 @@
|
|||
import Layout from '@/layout/index.vue'
|
||||
export default [
|
||||
|
||||
]
|
||||
|
||||
|
|
@ -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: false,
|
||||
fixed: false,
|
||||
background: '#f5f7f9'
|
||||
}
|
||||
}
|
||||
export default setting
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { createPinia } from 'pinia'
|
||||
import piniaPersist from 'pinia-plugin-persist'
|
||||
const store = createPinia()
|
||||
store.use(piniaPersist)
|
||||
|
||||
export function setupStore(app) {
|
||||
app.use(store)
|
||||
}
|
||||
|
|
@ -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,129 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { asyncRoutes, basicRoutes } from '@/router/routes'
|
||||
import { getMenu } from '@/api/auth/index'
|
||||
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)
|
||||
// }
|
||||
// }
|
||||
// 如果pid为0
|
||||
if (tmp.pid === 0) {
|
||||
// Layout引入
|
||||
tmp.component = Layout
|
||||
if (tmp.children) {
|
||||
tmp.children = dataArrayToRoutes(tmp.children)
|
||||
}
|
||||
} else {
|
||||
const sub_view = tmp.component.replace(/^\/*/g, '')
|
||||
const component = `../${sub_view}.vue`
|
||||
tmp.component = modules[component]
|
||||
}
|
||||
tmp.name = tmp.title
|
||||
tmp.meta = {
|
||||
...tmp.meta,
|
||||
title: tmp.meta.title || tmp.title
|
||||
}
|
||||
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)
|
||||
console.log(result)
|
||||
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,72 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { userLogin, loginOut } from '@/api/login'
|
||||
import { getUser } from '@/api/login/index.js'
|
||||
import { setToken, removeToken } from '@/utils/token'
|
||||
|
||||
import { useTagsMenuStore } from './tagsMenu.js'
|
||||
import { usePermissionStore } from './permission.js'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
persist: true,
|
||||
state() {
|
||||
return {
|
||||
userInfo: {}
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
userInfoMsg() {
|
||||
return this.userInfo
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
/* 登录 */
|
||||
async getLoginToken(form) {
|
||||
try {
|
||||
const res = await userLogin(form)
|
||||
if (res.code === 0) {
|
||||
/* 设置token */
|
||||
setToken(res.data.access_token)
|
||||
this.getUserInfo()
|
||||
return Promise.resolve(res)
|
||||
} else {
|
||||
return Promise.reject(res)
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
},
|
||||
/* 获取用户信息 */
|
||||
async getUserInfo() {
|
||||
const res = await getUser()
|
||||
if (res.code === 0) {
|
||||
this.setUserInfo(res.data)
|
||||
}
|
||||
},
|
||||
async userLogout() {
|
||||
try {
|
||||
const res = await loginOut()
|
||||
if (res.code === 0) {
|
||||
removeToken()
|
||||
this.reset()
|
||||
return Promise.resolve(res)
|
||||
} else {
|
||||
return Promise.reject(res)
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.$reset()
|
||||
const tagsMenuStore = useTagsMenuStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
tagsMenuStore.$reset()
|
||||
permissionStore.$reset()
|
||||
},
|
||||
|
||||
setUserInfo(userInfo = {}) {
|
||||
this.userInfo = { ...this.userInfo, ...userInfo }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
@import './reset.scss';
|
||||
@import './public.scss';
|
||||
@import './layout.scss';
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
.layout,.n-space{
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.layout{
|
||||
.layout__header{
|
||||
height: 48px;
|
||||
}
|
||||
.layout__content{
|
||||
height: calc(100vh - 48px);
|
||||
}
|
||||
}
|
||||
|
||||
.n-data-table .n-data-table-tr{
|
||||
th{
|
||||
white-space:nowrap;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,30 @@
|
|||
export const QUESTION_STATUS = [
|
||||
{ label: '已确认', value: 1 },
|
||||
{ label: '已忽略', value: 2 },
|
||||
{ label: '待确认', value: 3 }
|
||||
]
|
||||
|
||||
export const USER_STATUS = [
|
||||
{ label: '正常', value: 1 },
|
||||
{ label: '禁用', value: 2 }
|
||||
]
|
||||
|
||||
export const MENU_TYPE = [
|
||||
{ label: '菜单', value: 0 },
|
||||
{ label: '按钮', value: 1 }
|
||||
]
|
||||
|
||||
export const MENU_OPEN = [
|
||||
{ label: '内部', value: '1' },
|
||||
{ label: '外部', value: '2' }
|
||||
]
|
||||
|
||||
export const MENU_VISIBLE = [
|
||||
{ label: '可见', value: 0 },
|
||||
{ label: '不可见', value: 1 }
|
||||
]
|
||||
|
||||
export const MENU_STATUS = [
|
||||
{ label: '在用', value: 1 },
|
||||
{ label: '停用', value: 2 }
|
||||
]
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* pid形式数据转children形式
|
||||
* @param data 需要转换的数组
|
||||
* @param idKey id字段名
|
||||
* @param pidKey pid字段名
|
||||
* @param childKey 生成的children字段名
|
||||
* @param pid 顶级的pid
|
||||
* @param addPIds 是否添加所有父级id的字段
|
||||
* @param parentsKey 所有父级id的字段名称,默认parentIds
|
||||
* @param parentIds 所有父级id
|
||||
* @returns {[]}
|
||||
*/
|
||||
export function toTreeData(data, idKey, pidKey, childKey, pid, addPIds, parentsKey, parentIds) {
|
||||
if (typeof data === 'object' && !Array.isArray(data)) {
|
||||
idKey = data.idKey
|
||||
pidKey = data.pidKey
|
||||
childKey = data.childKey
|
||||
pid = data.pid
|
||||
addPIds = data.addPIds
|
||||
parentsKey = data.parentsKey
|
||||
parentIds = data.parentIds
|
||||
data = data.data
|
||||
}
|
||||
if (!childKey) {
|
||||
childKey = 'children'
|
||||
}
|
||||
if (typeof pid === 'undefined') {
|
||||
pid = []
|
||||
data.forEach((d) => {
|
||||
let flag = true
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (d[pidKey] === data[i][idKey]) {
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (flag) {
|
||||
pid.push(d[pidKey])
|
||||
}
|
||||
})
|
||||
}
|
||||
const result = []
|
||||
data.forEach((d) => {
|
||||
if (d[idKey] === d[pidKey]) {
|
||||
console.error('data error: ', d)
|
||||
return
|
||||
}
|
||||
if (Array.isArray(pid) ? (pid.indexOf(d[pidKey]) !== -1) : (d[pidKey] === pid)) {
|
||||
const children = toTreeData({
|
||||
data: data,
|
||||
idKey: idKey,
|
||||
pidKey: pidKey,
|
||||
childKey: childKey,
|
||||
pid: d[idKey],
|
||||
addPIds: addPIds,
|
||||
parentsKey: parentsKey,
|
||||
parentIds: (parentIds || []).concat([d[idKey]])
|
||||
})
|
||||
if (children.length > 0) {
|
||||
d[childKey] = children
|
||||
}
|
||||
if (addPIds) {
|
||||
d[parentsKey || 'parentIds'] = parentIds || []
|
||||
}
|
||||
result.push(d)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历children形式数据
|
||||
* @param data 需要遍历的数组
|
||||
* @param callback 回调
|
||||
* @param childKey children字段名
|
||||
*/
|
||||
export function eachTreeData(data, callback, childKey = 'children') {
|
||||
if (!data || !data.length) {
|
||||
return
|
||||
}
|
||||
data.forEach((d) => {
|
||||
if (callback(d) !== false && d[childKey]) {
|
||||
eachTreeData(d[childKey], callback, childKey)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理树形数据
|
||||
* @param data 需要处理的数据
|
||||
* @param formatter 处理器
|
||||
* @param childKey children字段名
|
||||
* @returns {[]} 处理后的数据
|
||||
*/
|
||||
export function formatTreeData(data, formatter, childKey = 'children') {
|
||||
const result = []
|
||||
if (data && data.length) {
|
||||
data.forEach((d) => {
|
||||
const item = formatter(d)
|
||||
if (item !== false) {
|
||||
if (item[childKey]) {
|
||||
item[childKey] = formatTreeData(item[childKey], formatter, childKey)
|
||||
}
|
||||
result.push(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理select数据
|
||||
* @param data 需要处理的数据
|
||||
* @params {label: XX, value: XX, children: XX} 需要处理的结构
|
||||
* @returns {[]} 处理后的数据
|
||||
*/
|
||||
export function dataToSelect(data, optionsObj) {
|
||||
const result = []
|
||||
if (data && data.length) {
|
||||
data.forEach((item) => {
|
||||
const i = {}
|
||||
i.label = item[optionsObj.label]
|
||||
i.value = item[optionsObj.value]
|
||||
if (item.children && item.children.length) {
|
||||
dataToSelect(item.children, optionsObj)
|
||||
i.children = item[optionsObj.children]
|
||||
}
|
||||
result.push(i)
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
@ -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,16 @@
|
|||
import axios from 'axios'
|
||||
import { setupInterceptor } from './interceptors'
|
||||
|
||||
const mockBaseURL = window.__APP__GLOB__CONF__?.VITE_APP_GLOB_BASE_API_MOCK || import.meta.env.VITE_APP_GLOB_BASE_API_MOCK
|
||||
const defBaseURL = window.__APP__GLOB__CONF__?.VITE_APP_GLOB_BASE_API || import.meta.env.VITE_APP_GLOB_BASE_API
|
||||
const baseURL = import.meta.env.VITE_APP_USE_MOCK === 'true' ? mockBaseURL : defBaseURL
|
||||
function createAxios(option = {}) {
|
||||
const service = axios.create({
|
||||
timeout: option.timeout || 120000,
|
||||
baseURL: (option.baseURL || defBaseURL) + import.meta.env.VITE_SERVER
|
||||
})
|
||||
setupInterceptor(service)
|
||||
return service
|
||||
}
|
||||
|
||||
export const defAxios = createAxios({ baseURL })
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
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()
|
||||
const token = 'token'
|
||||
if (token) {
|
||||
config.headers.Authorization = 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) => {
|
||||
const { method } = response?.config
|
||||
const { code } = response?.data
|
||||
const { currentRoute } = router
|
||||
switch (code) {
|
||||
case 0:
|
||||
if (method !== 'get') {
|
||||
$message.success(response.data.msg)
|
||||
}
|
||||
break
|
||||
case -1:
|
||||
$message.error(response.data.msg)
|
||||
break
|
||||
case 401:
|
||||
// 未登录(可能是token过期或者无效了)
|
||||
removeToken()
|
||||
router.replace({
|
||||
path: '/login',
|
||||
query: { ...currentRoute.query, redirect: currentRoute.path }
|
||||
})
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
return response?.data
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import { h } from 'vue'
|
||||
import { NIcon } from 'naive-ui'
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 渲染图标
|
||||
* @return {*}
|
||||
*/
|
||||
export function renderIcon(icon) {
|
||||
return () => h(NIcon, null, { default: () => h(icon) })
|
||||
}
|
||||
|
|
@ -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,25 @@
|
|||
<template>
|
||||
<div>
|
||||
首页
|
||||
</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,125 @@
|
|||
<template>
|
||||
<div class="login_bg">
|
||||
<n-form
|
||||
ref="formRef"
|
||||
:model="loginForm"
|
||||
:rules="rules"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
require-mark-placement="right-hanging"
|
||||
:style="{
|
||||
maxWidth: '640px',
|
||||
backgroundColor: '#fff',
|
||||
padding: '20px',
|
||||
borderRadius: '10px'
|
||||
}"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<n-form-item label="用户名" path="username">
|
||||
<n-input v-model:value="loginForm.username" placeholder="请输入用户名" />
|
||||
</n-form-item>
|
||||
<n-form-item label="密码" path="password">
|
||||
<n-input v-model:value="loginForm.password" type="password" placeholder="请输入用户名" />
|
||||
</n-form-item>
|
||||
<n-form-item label="验证码" path="captcha">
|
||||
<n-input v-model:value="loginForm.captcha" placeholder="请输入验证码" />
|
||||
<img v-if="captcha" :src="captcha" alt="" @click="changeCode">
|
||||
</n-form-item>
|
||||
<n-form-item path="remember">
|
||||
<n-checkbox v-model:checked="loginForm.remember" size="medium" label="记住密码" />
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button @click="handleLogin">登录</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { userLogin, userCaptcha } from '@/api/login/index.js'
|
||||
import { setToken } from '@/utils/token'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
export default {
|
||||
name: 'LoginPage',
|
||||
setup() {
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
key: '',
|
||||
captcha: '',
|
||||
remember: false
|
||||
})
|
||||
onMounted(() => {
|
||||
changeCode(loginForm)
|
||||
})
|
||||
// 获取图形验证码
|
||||
const captcha = ref('')
|
||||
async function changeCode(form) {
|
||||
const params = {
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
captcha: form.captcha
|
||||
}
|
||||
const captForm = await userCaptcha(params)
|
||||
captcha.value = captForm.data.captcha
|
||||
loginForm.key = captForm.data.key
|
||||
}
|
||||
|
||||
return {
|
||||
loginForm,
|
||||
captcha,
|
||||
changeCode,
|
||||
rules: reactive({
|
||||
username: {
|
||||
required: true,
|
||||
trigger: ['blur', 'input'],
|
||||
message: '请输入用户名'
|
||||
},
|
||||
password: {
|
||||
required: true,
|
||||
trigger: ['blur', 'input'],
|
||||
message: '请输入密码'
|
||||
},
|
||||
captcha: {
|
||||
required: true,
|
||||
trigger: ['blur', 'input'],
|
||||
message: '请输入验证码'
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleLogin() {
|
||||
console.log(this.loginForm)
|
||||
userLogin(this.loginForm).then((res) => {
|
||||
if (res.code === 0) {
|
||||
// 登录成功存储token,跳转首页
|
||||
setToken(res.data.access_token)
|
||||
this.goPage()
|
||||
} else {
|
||||
console.log(res)
|
||||
}
|
||||
})
|
||||
},
|
||||
// 登录成功跳转
|
||||
goPage() {
|
||||
// const query = this.$route.query
|
||||
// const path = query && query.from ? query.from : '/'
|
||||
// console.log(path)
|
||||
// this.$router.replace(path)
|
||||
this.$router.replace('/home')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login_bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<script>
|
||||
import { defineComponent, onBeforeMount } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RedirectPage',
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
onBeforeMount(() => {
|
||||
const { params, query } = route
|
||||
const { path } = params
|
||||
router.replace({
|
||||
path: '/' + (Array.isArray(path) ? path.join('/') : path),
|
||||
query
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue