@@ -0,0 +1,7 @@ | |||
# title | |||
VITE_APP_TITLE = 'h5' | |||
# 端口号 | |||
VITE_PORT = 3000 | |||
VITE_SERVER = "/" |
@@ -0,0 +1,11 @@ | |||
# 资源公共路径,需要以 /开头和结尾 | |||
VITE_PUBLIC_PATH = '/' | |||
# 是否启用MOCK | |||
VITE_APP_USE_MOCK = false | |||
# proxy | |||
VITE_PROXY = [["/api","http://192.168.11.11:7011/api"]] | |||
# base api | |||
VITE_APP_GLOB_BASE_API = '/api' |
@@ -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"],["/api-mock","http://127.0.0.1:8003"]] | |||
# base api | |||
VITE_APP_GLOB_BASE_API = '/api-local' | |||
# mock base api | |||
VITE_APP_GLOB_BASE_API_MOCK = '/api-mock' |
@@ -0,0 +1,11 @@ | |||
# 资源公共路径,需要以 /开头和结尾 | |||
VITE_PUBLIC_PATH = '/' | |||
# 是否启用MOCK | |||
VITE_APP_USE_MOCK = false | |||
# proxy | |||
VITE_PROXY = [["/api","https://dsp-portal.t-aaron.com/api"]] | |||
# base api | |||
VITE_APP_GLOB_BASE_API = '/api' |
@@ -0,0 +1,11 @@ | |||
# 资源公共路径,需要以 /开头和结尾 | |||
VITE_PUBLIC_PATH = '/' | |||
# 是否启用MOCK | |||
VITE_APP_USE_MOCK = false | |||
# proxy | |||
VITE_PROXY = [["/api","http://192.168.11.241:7011/api"]] | |||
# base api | |||
VITE_APP_GLOB_BASE_API = '/api' |
@@ -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": ["Vue.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 | |||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.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,33 @@ | |||
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 | |||
console.log('VITE_PUBLIC_PATH', VITE_PUBLIC_PATH) | |||
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,25 @@ | |||
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,18 @@ | |||
<!DOCTYPE html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="UTF-8" /> | |||
<meta http-equiv="Expires" content="0" /> | |||
<meta http-equiv="Pragma" content="no-cache" /> | |||
<meta http-equiv="Cache-control" content="no-cache" /> | |||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||
<link rel="icon" href="/favicon.ico" /> | |||
<title><%= title %></title> | |||
</head> | |||
<body> | |||
<div id="app"></div> | |||
<script type="module" src="/src/main.js"></script> | |||
</body> | |||
</html> |
@@ -0,0 +1,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,12 @@ | |||
export function resolveToken(authorization) { | |||
/** | |||
* * jwt token | |||
* * Bearer + token | |||
* ! 认证方案: Bearer | |||
*/ | |||
const reqTokenSplit = authorization.split(' ') | |||
if (reqTokenSplit.length === 2) { | |||
return reqTokenSplit[1] | |||
} | |||
return '' | |||
} |
@@ -0,0 +1,47 @@ | |||
{ | |||
"name": "vite-project", | |||
"version": "0.0.0", | |||
"scripts": { | |||
"dev": "vite --mode localhost", | |||
"build:test": "vite build --mode test && esno ./build/script", | |||
"build:dev": "vite build --mode development && esno ./build/script", | |||
"build:prod": "vite build --mode production && esno ./build/script", | |||
"serve": "vite preview" | |||
}, | |||
"dependencies": { | |||
"@vicons/antd": "^0.10.0", | |||
"@vicons/ionicons5": "^0.10.0", | |||
"axios": "^0.26.1", | |||
"dayjs": "^1.11.2", | |||
"mockjs": "^1.1.0", | |||
"pinia": "^2.0.13", | |||
"pinia-plugin-persist": "^1.0.0", | |||
"trtc-js-sdk": "^4.14.4", | |||
"vue": "^3.2.37", | |||
"vue-router": "^4.0.14" | |||
}, | |||
"devDependencies": { | |||
"@unocss/preset-attributify": "^0.16.4", | |||
"@unocss/preset-icons": "^0.16.4", | |||
"@unocss/preset-uno": "^0.16.4", | |||
"@vitejs/plugin-legacy": "^1.8.2", | |||
"@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.33.5", | |||
"sass": "^1.49.11", | |||
"unocss": "^0.16.4", | |||
"unplugin-vue-components": "^0.18.5", | |||
"vite": "^2.6.4", | |||
"vite-plugin-html": "^2.1.2", | |||
"vite-plugin-mock": "^2.9.6", | |||
"vite-plugin-vue-setup-extend": "^0.4.0" | |||
} | |||
} |
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> |
@@ -0,0 +1,33 @@ | |||
<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 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg> |
@@ -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,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,81 @@ | |||
<template> | |||
<div> | |||
11 | |||
</div> | |||
</template> | |||
<script> | |||
import TRTC from 'trtc-js-sdk' | |||
import { reactive } from 'vue' | |||
export default { | |||
name: 'MeetingPage', | |||
setup() { | |||
const data = reactive({ | |||
sdkAppId: 1400752641, | |||
sdkSecret: '9b5fc557f286d7e4d6eafd8023026da59f0674000f319754aa1ec4beefddcdd6', | |||
client: null, | |||
userId: 'wanghaoran', | |||
userSig: 'eJwtzEELgjAcBfDvsnO4ubnFhA6ihwIhUqEOXhZO*6dNUalF9N0z9fh*7-E*KItT56l75CPqELSZMxTajFDCzC9lqptqe2XWdihq1XVQIN-1CNlyKjx3abTtoNeTc84pIWTRER5-E0IwVwq5bgeopnPPRDluaVPGTb1P3nd7CcPMHgJ2zbFJ5HCKrDkHxzLHMmX1Dn1-G3A0Zg__' | |||
}) | |||
const createMeetingRoom = async(userId, userSig) => { | |||
data.client = TRTC.createClient({ | |||
sdkAppId: data.sdkAppId, // 填写您申请的 sdkAppId | |||
userId: userId, // 填写您业务对应的 userId | |||
userSig: userSig, // 填写服务器或本地计算的 userSig | |||
mode: 'rtc' | |||
}) | |||
const localStream = TRTC.createStream({ userId, audio: true, video: true }) | |||
data.client.on('stream-added', event => { | |||
const remoteStream = event.stream | |||
console.log('远端流增加: ' + remoteStream.getId()) | |||
// 订阅远端流 | |||
data.client.subscribe(remoteStream) | |||
}) | |||
data.client.on('stream-subscribed', event => { | |||
const remoteStream = event.stream | |||
console.log('远端流订阅成功:' + remoteStream.getId()) | |||
// 播放远端流 | |||
remoteStream.play('remote-stream-' + remoteStream.getId()) | |||
}) | |||
data.client.on('client-banned', error => { | |||
console.error('client-banned observed: ' + error.message) | |||
}) | |||
/* 进入房间 */ | |||
try { | |||
await data.client.join({ roomId: 111 }) | |||
console.log('进房成功') | |||
} catch (error) { | |||
console.error('进房失败,请稍后再试' + error) | |||
} | |||
try { | |||
await localStream.initialize() | |||
localStream.play('local_stream') | |||
console.log('初始化本地流成功') | |||
} catch (error) { | |||
console.error('初始化本地流失败 ' + error) | |||
} | |||
try { | |||
await data.client.publish(localStream) | |||
console.log('本地流发布成功') | |||
} catch (error) { | |||
console.error('本地流发布失败 ' + error) | |||
} | |||
} | |||
const leaveMeetingRoom = async() => { | |||
await data.client.leave() | |||
} | |||
// createMeetingRoom(data.userId, data.userSig) | |||
} | |||
} | |||
</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,66 @@ | |||
<template> | |||
<n-layout-header class="layout__header" bordered> | |||
<n-dropdown trigger="hover" :options="options"> | |||
<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 } from 'vue' | |||
import { EditOutlined, LogoutOutlined } from '@vicons/antd' | |||
import { renderIcon } from '@/utils' | |||
export default defineComponent({ | |||
name: 'LayoutHeader', | |||
setup() { | |||
const data = reactive({ | |||
options: [ | |||
{ | |||
label: '修改密码', | |||
key: 'edit', | |||
icon: renderIcon(EditOutlined) | |||
}, | |||
{ | |||
label: '退出登录', | |||
key: 'out', | |||
icon: renderIcon(LogoutOutlined) | |||
} | |||
], | |||
userInfo: { | |||
avatar: '', | |||
realname: '管理员' | |||
} | |||
}) | |||
return { | |||
...toRefs(data) | |||
} | |||
} | |||
}) | |||
</script> | |||
<style scoped> | |||
.layout__header { | |||
padding: 0 20px; | |||
display: flex; | |||
justify-content: flex-end; | |||
align-items: center; | |||
} | |||
.user_msg { | |||
display: flex; | |||
justify-content: flex-end; | |||
align-items: center; | |||
} | |||
.user_avatar { | |||
width: 30px; | |||
height: 30px; | |||
margin-right: 10px; | |||
} | |||
</style> |
@@ -0,0 +1,33 @@ | |||
<template> | |||
<n-layout-sider | |||
class="layout_sidebar" | |||
v-bind="getSideOptions" | |||
> | |||
<div class="project__logo"> | |||
<n-image :width="getLogoWidth" src="/logo.png" preview-disabled /> | |||
</div> | |||
<SideMenu /> | |||
</n-layout-sider> | |||
</template> | |||
<script setup> | |||
import { computed, toRaw } from 'vue' | |||
import SideMenu from '@/layout/components/Menu/index.vue' | |||
import { useSettingStore } from '@/store/modules/setting.js' | |||
const settingStore = useSettingStore() | |||
const getSideOptions = computed(() => { | |||
return toRaw(settingStore.getSidebarSetting) | |||
}) | |||
const getLogoWidth = computed(() => { | |||
return settingStore.getSidebarSetting.width - 30 | |||
}) | |||
</script> | |||
<style lang="scss" scoped> | |||
.project__logo{ | |||
padding: 20px 15px; | |||
} | |||
</style> |
@@ -0,0 +1,40 @@ | |||
<template> | |||
<n-space class="layout" :size="0" vertical> | |||
<n-layout> | |||
<!-- <n-layout-header class="layout__header" :class="{'has--shadow':isHome}"> | |||
<Header /> | |||
</n-layout-header> --> | |||
<n-layout-content class="layout__content"> | |||
<router-view /> | |||
<!-- <Footer /> --> | |||
</n-layout-content> | |||
</n-layout> | |||
</n-space> | |||
</template> | |||
<script > | |||
// import Header from './components/Header/index.vue' | |||
import { ref, watchEffect } from 'vue' | |||
import { useRoute } from 'vue-router' | |||
export default { | |||
name: 'LayoutFooter', | |||
// components: { Header }, | |||
setup() { | |||
const route = useRoute() | |||
const isHome = ref(false) | |||
watchEffect(() => { | |||
isHome.value = !(['home', 'empty'].includes(route.name)) | |||
}) | |||
return { | |||
isHome | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
</style> |
@@ -0,0 +1,19 @@ | |||
import '@/styles/index.scss' | |||
import 'uno.css' | |||
import { createApp } from 'vue' | |||
import { setupRouter } from '@/router' | |||
import { setupStore } from '@/store' | |||
import App from './App.vue' | |||
function setupApp() { | |||
const app = createApp(App) | |||
setupStore(app) | |||
setupRouter(app) | |||
app.mount('#app') | |||
} | |||
setupApp() |
@@ -0,0 +1,9 @@ | |||
import { createPageLoadingGuard } from './page-loading-guard' | |||
import { createPageTitleGuard } from './page-title-guard' | |||
import { createPermissionGuard } from './permission-guard' | |||
export function setupRouterGuard(router) { | |||
createPageLoadingGuard(router) | |||
createPermissionGuard(router) | |||
createPageTitleGuard(router) | |||
} |
@@ -0,0 +1,15 @@ | |||
export function createPageLoadingGuard(router) { | |||
router.beforeEach(() => { | |||
window.$loadingBar?.start() | |||
}) | |||
router.afterEach(() => { | |||
setTimeout(() => { | |||
window.$loadingBar?.finish() | |||
}, 200) | |||
}) | |||
router.onError(() => { | |||
window.$loadingBar?.error() | |||
}) | |||
} |
@@ -0,0 +1,12 @@ | |||
const baseTitle = import.meta.env.VITE_APP_TITLE | |||
export function createPageTitleGuard(router) { | |||
router.afterEach((to) => { | |||
const pageTitle = to.meta?.title | |||
if (pageTitle) { | |||
document.title = `${pageTitle} | ${baseTitle}` | |||
} else { | |||
document.title = baseTitle | |||
} | |||
}) | |||
} |
@@ -0,0 +1,57 @@ | |||
// import { useNavMenuStore } from '@/store/modules/navMenu' | |||
// 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 navMenuStore = useNavMenuStore() | |||
// const permissionStore = usePermissionStore() | |||
router.beforeEach(async(to, from, next) => { | |||
/* 如果是白名单,则判断是否已加载导航数据 */ | |||
// if (WHITE_LIST.includes(to.path)) { | |||
if (to.meta.isWhite) { | |||
// const hasNavRoutes = !!navMenuStore.menus.length | |||
// if (hasNavRoutes) { | |||
next() | |||
// } else { | |||
// try { | |||
// await navMenuStore.generateMenus() | |||
// router.addRoute(NOT_FOUND_ROUTE) | |||
// router.addRoute(REDIRECT_ROUTE) | |||
// next({ ...to, replace: true }) | |||
// } catch (error) { | |||
// next({ path: '/login', query: { ...to.query, redirect: to.path }}) | |||
// } | |||
// } | |||
} else { | |||
/* 判断访问白名单外的页面是否存在token */ | |||
// const token = getToken() | |||
const token = true | |||
if (token) { | |||
/* 如果存在token,登录时直接进入主页 */ | |||
if (to.path === '/login') { | |||
next({ path: '/' }) | |||
} else { | |||
// const hasRoutes = !!permissionStore.permissionRoutes.length | |||
const hasRoutes = true | |||
if (hasRoutes) { | |||
next() | |||
} else { | |||
// try { | |||
// const routes = await permissionStore.generateRoutes() | |||
// routes.forEach((item) => { | |||
// router.addRoute(item) | |||
// }) | |||
// next({ ...to, replace: true }) | |||
// } catch (error) { | |||
// next({ path: '/login', query: { ...to.query, redirect: to.path }}) | |||
// } | |||
} | |||
} | |||
} 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,76 @@ | |||
import Layout from '@/layout/index.vue' | |||
import Home from '@/views/home/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: '登录页', | |||
isWhite: true | |||
} | |||
}, | |||
{ | |||
path: '/', | |||
title: '首页', | |||
component: Layout, | |||
redirect: '/home', | |||
meta: { | |||
title: '首页', | |||
isWhite: true | |||
}, | |||
children: [ | |||
{ | |||
path: 'home', | |||
title: 'Home', | |||
component: Home, | |||
name: 'home', | |||
meta: { | |||
title: '首页', | |||
affix: true, | |||
isWhite: 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,4 @@ | |||
export default [ | |||
] | |||
@@ -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,30 @@ | |||
import { defineStore } from 'pinia' | |||
export const useMenuStore = defineStore('menu', { | |||
persist: { | |||
enabled: true | |||
}, | |||
state() { | |||
return { | |||
menuInfo: { | |||
MID: 'home', | |||
FID: null, | |||
SID: null, | |||
TID: null | |||
} | |||
} | |||
}, | |||
getters: { | |||
menuInfoMsg() { | |||
return this.menuInfo | |||
} | |||
}, | |||
actions: { | |||
async setMenuInfo(info) { | |||
this.menuInfo = info | |||
}, | |||
reset() { | |||
this.$reset() | |||
} | |||
} | |||
}) |
@@ -0,0 +1,123 @@ | |||
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: { | |||
async generateRoutes() { | |||
try { | |||
// const res = await getMenu() | |||
// if (res.code === 0) { | |||
// const result = dataArrayToRoutes(res.data) | |||
// this.accessRoutes = result | |||
// return Promise.resolve(result) | |||
// } else { | |||
// return Promise.reject(res.message) | |||
// } | |||
} catch (error) { | |||
console.error(error) | |||
return Promise.reject(error.message) | |||
} | |||
} | |||
} | |||
}) |
@@ -0,0 +1,25 @@ | |||
import { defineStore } from 'pinia' | |||
import config from '@/setting/config.js' | |||
export const useSettingStore = defineStore({ | |||
id: 'project-setting', | |||
state: () => ({ | |||
...config | |||
}), | |||
getters: { | |||
getMenuMode() { | |||
return this.menuMode | |||
}, | |||
getHeaderSetting() { | |||
return this.headerSetting | |||
}, | |||
getSidebarSetting() { | |||
return this.sidebarSetting | |||
}, | |||
getTagsMenuSetting() { | |||
return this.tagsMenuSetting | |||
} | |||
}, | |||
actions: { | |||
} | |||
}) |
@@ -0,0 +1,56 @@ | |||
import { defineStore } from 'pinia' | |||
// 不需要出现在标签页中的路由 | |||
const whiteList = ['Redirect', 'login'] | |||
// 保留固定路由 | |||
function retainAffixRoute(list) { | |||
return list.filter((item) => item?.meta?.affix ?? false) | |||
} | |||
export const useTagsMenuStore = defineStore({ | |||
id: 'project-tags-menu', | |||
state: () => ({ | |||
tabsList: [] | |||
}), | |||
getters: {}, | |||
actions: { | |||
initTabs(routes) { | |||
// 初始化标签页 | |||
this.tabsList = routes | |||
}, | |||
addTabs(route) { | |||
// 添加标签页 | |||
if (whiteList.includes(route.name)) return false | |||
const isExists = this.tabsList.some((item) => item.fullPath === route.fullPath) | |||
if (!isExists) { | |||
this.tabsList.push(route) | |||
} | |||
return true | |||
}, | |||
closeLeftTabs(route) { | |||
// 关闭左侧 | |||
const index = this.tabsList.findIndex((item) => item.fullPath === route.fullPath) | |||
this.tabsList.splice(0, index) | |||
}, | |||
closeRightTabs(route) { | |||
// 关闭右侧 | |||
const index = this.tabsList.findIndex((item) => item.fullPath === route.fullPath) | |||
this.tabsList.splice(index + 1) | |||
}, | |||
closeOtherTabs(route) { | |||
// 关闭其他 | |||
this.tabsList = this.tabsList.filter((item) => item.fullPath === route.fullPath) | |||
}, | |||
closeCurrentTab(route) { | |||
// 关闭当前页 | |||
const index = this.tabsList.findIndex((item) => item.fullPath === route.fullPath) | |||
this.tabsList.splice(index, 1) | |||
}, | |||
closeAllTabs() { | |||
// 关闭全部 | |||
console.log(retainAffixRoute(this.tabsList)) | |||
this.tabsList = retainAffixRoute(this.tabsList) | |||
} | |||
} | |||
}) |
@@ -0,0 +1,53 @@ | |||
import { defineStore } from 'pinia' | |||
import { getUserInfo } from '@/utils/oidc/index.js' | |||
export const useUserStore = defineStore('user', { | |||
persist: { | |||
enabled: true | |||
}, | |||
state() { | |||
return { | |||
userInfo: { | |||
hasLogin: false | |||
} | |||
} | |||
}, | |||
getters: { | |||
// userId() { | |||
// return this.userInfo?.id | |||
// }, | |||
userName() { | |||
return this.userInfo?.userName | |||
}, | |||
avatar() { | |||
return this.userInfo?.avatar | |||
}, | |||
role() { | |||
return this.userInfo?.role || [] | |||
}, | |||
authority() { | |||
return this.userInfo.authority | |||
}, | |||
hasLogin() { | |||
return this.userInfo.hasLogin | |||
} | |||
}, | |||
actions: { | |||
async getUserInfos() { | |||
try { | |||
const res = await getUserInfo() | |||
if (res) { | |||
this.setUserInfo({ hasLogin: true, ...res.profile }) | |||
} else { | |||
this.setUserInfo() | |||
} | |||
} catch (error) { | |||
console.error(error) | |||
this.setUserInfo() | |||
} | |||
}, | |||
setUserInfo(userInfo = { hasLogin: false }) { | |||
this.userInfo = { ...this.userInfo, ...userInfo } | |||
} | |||
} | |||
}) |
@@ -0,0 +1,90 @@ | |||
:root { | |||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif; | |||
font-size: 16px; | |||
line-height: 24px; | |||
font-weight: 400; | |||
color-scheme: light dark; | |||
color: rgba(255, 255, 255, 0.87); | |||
background-color: #242424; | |||
font-synthesis: none; | |||
text-rendering: optimizeLegibility; | |||
-webkit-font-smoothing: antialiased; | |||
-moz-osx-font-smoothing: grayscale; | |||
-webkit-text-size-adjust: 100%; | |||
} | |||
a { | |||
font-weight: 500; | |||
color: #646cff; | |||
text-decoration: inherit; | |||
} | |||
a:hover { | |||
color: #535bf2; | |||
} | |||
a { | |||
font-weight: 500; | |||
color: #646cff; | |||
text-decoration: inherit; | |||
} | |||
a:hover { | |||
color: #535bf2; | |||
} | |||
body { | |||
margin: 0; | |||
display: flex; | |||
place-items: center; | |||
min-width: 320px; | |||
min-height: 100vh; | |||
} | |||
h1 { | |||
font-size: 3.2em; | |||
line-height: 1.1; | |||
} | |||
button { | |||
border-radius: 8px; | |||
border: 1px solid transparent; | |||
padding: 0.6em 1.2em; | |||
font-size: 1em; | |||
font-weight: 500; | |||
font-family: inherit; | |||
background-color: #1a1a1a; | |||
cursor: pointer; | |||
transition: border-color 0.25s; | |||
} | |||
button:hover { | |||
border-color: #646cff; | |||
} | |||
button:focus, | |||
button:focus-visible { | |||
outline: 4px auto -webkit-focus-ring-color; | |||
} | |||
.card { | |||
padding: 2em; | |||
} | |||
#app { | |||
max-width: 1280px; | |||
margin: 0 auto; | |||
padding: 2rem; | |||
text-align: center; | |||
} | |||
@media (prefers-color-scheme: light) { | |||
:root { | |||
color: #213547; | |||
background-color: #ffffff; | |||
} | |||
a:hover { | |||
color: #747bff; | |||
} | |||
button { | |||
background-color: #f9f9f9; | |||
} | |||
} |
@@ -0,0 +1,3 @@ | |||
@import './reset.scss'; | |||
@import './public.scss'; | |||
@import './layout.scss'; |
@@ -0,0 +1,32 @@ | |||
.layout,.n-space{ | |||
height: inherit; | |||
} | |||
.layout{ | |||
.n-layout,.layout__header{ | |||
overflow: visible; | |||
} | |||
.layout__header{ | |||
height: 48px; | |||
position: relative; | |||
z-index: 99; | |||
&.has--shadow{ | |||
box-shadow: 0px 9px 6px 1px rgba(216, 216, 216, 1); | |||
} | |||
} | |||
.layout__content{ | |||
// height: calc(100vh - 48px); | |||
height: 100%; | |||
} | |||
} | |||
.n-data-table .n-data-table-tr{ | |||
th{ | |||
white-space:nowrap; | |||
} | |||
} | |||
.n-icon.board-icon{ | |||
cursor: pointer; | |||
margin: 10px !important; | |||
} |
@@ -0,0 +1,57 @@ | |||
html { | |||
font-size: 4px; // * 1rem = 4px 方便unocss计算:在unocss中 1字体单位 = 0.25rem,相当于 1等份 = 1px | |||
} | |||
html, | |||
body { | |||
width: 100%; | |||
min-width: 1440px; | |||
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,22 @@ | |||
export const VALUE_TYPE = [ | |||
{ label: '字符串', value: 1 }, | |||
{ label: '数值', value: 2 }, | |||
{ label: '日期时间', value: 3 }, | |||
{ label: '其他', value: 4 } | |||
] | |||
export const VALUE_NUMBER = [ | |||
{ label: '单值', value: 1 }, | |||
{ label: '多值', value: 2 } | |||
] | |||
export const SERVICE_MODE = [ | |||
{ label: '实时', value: 1 }, | |||
{ label: '离线', value: 2 }, | |||
{ label: '图片', value: 3 } | |||
] | |||
export const IS_REQUIRED = [ | |||
{ label: '非必填', value: 0 }, | |||
{ label: '必填', value: 1 } | |||
] |
@@ -0,0 +1,17 @@ | |||
import { useUserStore } from '@/store/modules/user' | |||
const WITHOUT_TOKEN_API = [ | |||
{ url: /^\/serviceDef\/portal\/getServiceDefList/, method: 'GET' }, | |||
{ url: /^\/serviceInst\/portal\/getServiceInstList/, method: 'GET' }, | |||
{ url: /^\/serviceInst\/portal\/getServiceInstSummary\/[a-z0-9]+$/, method: 'GET' } | |||
] | |||
export function isWithoutToken({ url, method = '' }) { | |||
return WITHOUT_TOKEN_API.some((item) => item.url.test(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,76 @@ | |||
import { router } from '@/router' | |||
import { removeToken } from '@/utils/token' | |||
import { isWithoutToken } from './help' | |||
import { getUserInfo, signoutRedirect } from '@/utils/oidc/index.js' | |||
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 | |||
} else { | |||
const userInfo = await getUserInfo() | |||
if (userInfo) { | |||
const { token_type, access_token } = userInfo | |||
config.headers.Authorization = `${token_type} ${access_token}` | |||
return config | |||
} else { | |||
signoutRedirect() | |||
return Promise.reject({ response: { status: 401, message: '未登录' }}) | |||
} | |||
} | |||
}, | |||
(error) => Promise.reject(error) | |||
) | |||
service.interceptors.response.use( | |||
(response) => { | |||
const { method, hideMessage = false } = response?.config | |||
const { code } = response?.data | |||
const { currentRoute } = router | |||
switch (code) { | |||
case 0: | |||
if (method !== 'get' && !hideMessage) { | |||
$message.success(response.data.msg) | |||
} | |||
break | |||
case 200: | |||
$message.error(response.data.msg) | |||
break | |||
case -1: | |||
$message.error(response.data.msg) | |||
break | |||
case 400: | |||
$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) => { | |||
const { status } = error.response | |||
if (status === 401) { | |||
signoutRedirect() | |||
} else if (status === 403) { | |||
$message.error('暂无权限访问,请联系管理员') | |||
return Promise.reject(error) | |||
} else { | |||
return Promise.reject(error) | |||
} | |||
} | |||
) | |||
} |
@@ -0,0 +1,85 @@ | |||
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,40 @@ | |||
import { createLocalStorage } from './cache' | |||
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, duration = DURATION) { | |||
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,7 @@ | |||
export const Button = { | |||
/* 按钮的颜色 */ | |||
color: 'rgba(56, 174, 243, 1)', | |||
/* 按钮文本颜色 */ | |||
textColor: 'rgba(56, 174, 243, 1)' | |||
} | |||
@@ -0,0 +1,56 @@ | |||
const common = { | |||
/* 主要颜色 */ | |||
primaryColor: 'rgba(18, 87, 250, 1)', | |||
/* 悬浮颜色 */ | |||
primaryColorHover: 'rgba(56, 114, 251, 1)', | |||
/* 按压的颜色 */ | |||
primaryColorPressed: 'rgba(16, 77, 220, 1)', | |||
textColor: 'rgba(51, 51, 51, 1)', | |||
whiteColor: 'rgba(255, 255, 255, 1)' | |||
} | |||
const themeOverrides = { | |||
common, | |||
Menu: { | |||
/* 菜单文本 */ | |||
itemTextColor: common.textColor, | |||
itemTextColorHover: common.textColor, | |||
itemTextColorActive: common.textColor, | |||
itemTextColorChildActive: common.textColor, | |||
itemTextColorActiveHover: common.textColor, | |||
/* 图标 */ | |||
itemIconColor: 'rgb(31, 34, 37)', | |||
itemIconColorHover: 'rgb(31, 34, 37)', | |||
itemIconColorActive: common.primaryColor, | |||
itemIconColorActiveHover: common.primaryColor, | |||
itemIconColorChildActive: common.primaryColor, | |||
itemIconColorCollapsed: 'rgb(31, 34, 37)', | |||
itemTextColorHorizontal: 'rgb(51, 54, 57)', | |||
itemTextColorHoverHorizontal: common.primaryColor, | |||
itemTextColorActiveHorizontal: common.primaryColor, | |||
itemTextColorChildActiveHorizontal: common.primaryColor, | |||
itemTextColorActiveHoverHorizontal: common.primaryColor, | |||
itemIconColorHorizontal: 'rgb(31, 34, 37)', | |||
itemIconColorHoverHorizontal: common.primaryColor, | |||
itemIconColorActiveHorizontal: common.primaryColor, | |||
itemIconColorActiveHoverHorizontal: common.primaryColor, | |||
itemIconColorChildActiveHorizontal: common.primaryColor, | |||
/* 箭头 */ | |||
arrowColor: common.textColor, | |||
arrowColorHover: common.textColor, | |||
arrowColorActive: 'rgba(0, 113, 183, 1)', | |||
arrowColorActiveHover: 'rgba(0, 113, 183, 1)', | |||
arrowColorChildActive: 'rgba(0, 113, 183, 1)', | |||
/* 菜单 */ | |||
itemColorHover: 'rgba(51, 112, 255, 0.05)', | |||
itemColorActive: 'rgba(51, 112, 255, 0.05)', | |||
itemColorActiveHover: 'rgba(51, 112, 255, 0.05)', | |||
itemColorActiveCollapsed: 'rgba(51, 112, 255, 0.05)' | |||
}, | |||
Tabs: { | |||
colorSegment: 'rgba(228, 228, 228, 1)', | |||
tabColorSegment: common.primaryColor | |||
} | |||
} | |||
export default themeOverrides |
@@ -0,0 +1,109 @@ | |||
<template> | |||
<div class="select-container"> | |||
<n-select | |||
v-model:value="activeDeviceId" | |||
class="select" | |||
:placeholder="deviceType" | |||
:options="deviceList" | |||
value-field="deviceId" | |||
@update:value="handleChange" | |||
/> | |||
</div> | |||
</template> | |||
<script> | |||
import TRTC from 'trtc-js-sdk' | |||
import { reactive, toRefs, onMounted, defineComponent } from 'vue' | |||
export default defineComponent({ | |||
name: 'DeviceSelect', | |||
props: { | |||
deviceType: { | |||
type: String, | |||
default: () => {} | |||
}, | |||
value: { | |||
type: String, | |||
default: () => {} | |||
} | |||
}, | |||
emits: ['update:value'], | |||
setup(props, { emit }) { | |||
const data = reactive({ | |||
deviceList: [], | |||
activeDeviceId: '' | |||
}) | |||
const getDeviceList = async() => { | |||
switch (props.deviceType) { | |||
case 'camera': | |||
data.deviceList = await TRTC.getCameras() | |||
break | |||
case 'microphone': | |||
data.deviceList = await TRTC.getMicrophones() | |||
break | |||
case 'speaker': | |||
data.deviceList = await TRTC.getSpeakers() | |||
break | |||
default: | |||
break | |||
} | |||
data.activeDeviceId = data.deviceList[0].deviceId | |||
emit('update:value', data.activeDeviceId) | |||
} | |||
const handleChange = (value) => { | |||
data.activeDeviceId = value | |||
emit('update:value', data.activeDeviceId) | |||
} | |||
onMounted(() => { | |||
navigator.mediaDevices.getUserMedia({ audio: true, video: true }) | |||
.then(() => { | |||
getDeviceList() | |||
}) | |||
// navigator.mediaDevices.addEventListener('devicechange', this.getDeviceList) | |||
}) | |||
// beforeUnmount() { | |||
// navigator.mediaDevices.removeEventListener('devicechange', this.getDeviceList) | |||
// } | |||
return { | |||
...toRefs(data), | |||
handleChange | |||
} | |||
} | |||
}) | |||
</script> | |||
<style lang="scss" scoped> | |||
.select-container { | |||
display: flex; | |||
.label { | |||
display: inline-block; | |||
padding: 0 20px; | |||
width: 120px; | |||
height: 40px; | |||
text-align: left; | |||
line-height: 40px; | |||
border-top: 1px solid #DCDFE6; | |||
border-left: 1px solid #DCDFE6; | |||
border-bottom: 1px solid #DCDFE6; | |||
border-radius: 4px 0 0 4px; | |||
color: #909399; | |||
background-color: #F5F7FA; | |||
font-weight: bold; | |||
} | |||
.select { | |||
flex-grow: 1; | |||
} | |||
} | |||
</style> | |||
<style lang="scss"> | |||
.select { | |||
input { | |||
border-radius: 0 4px 4px 0 !important; | |||
} | |||
} | |||
</style> |
@@ -0,0 +1,221 @@ | |||
<template> | |||
<div class="room"> | |||
<DeviceSelect v-show="false" v-model:value="cameraId" device-type="camera" /> | |||
<DeviceSelect v-show="false" v-model:value="microphoneId" device-type="microphone" /> | |||
<div v-if="localStream" class="local-stream-container"> | |||
<!-- 本地流播放区域 --> | |||
<div id="localStream" class="local-stream-content" /> | |||
<!-- 本地流操作栏 --> | |||
<div v-if="isPlayingLocalStream" class="local-stream-control"> | |||
<div class="video-control control"> | |||
<n-icon v-if="!isMutedVideo" size="40" @click="handleVideoMute"> | |||
<VideocamOutline /> | |||
</n-icon> | |||
<n-icon v-if="isMutedVideo" size="40" @click="handleVideoUnMute"> | |||
<VideocamOffOutline /> | |||
</n-icon> | |||
</div> | |||
<div class="audio-control control"> | |||
<n-icon v-if="!isMutedAudio" size="40" @click="handleAudioMute"> | |||
<AudioOutlined /> | |||
</n-icon> | |||
<n-icon v-if="isMutedAudio" size="40" @click="handleAudioUnMute"> | |||
<AudioMutedOutlined /> | |||
</n-icon> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import TRTC from 'trtc-js-sdk' | |||
import { useRoute } from 'vue-router' | |||
import DeviceSelect from './components/Device.vue' | |||
import { reactive, toRefs, onMounted, watch } from 'vue' | |||
import { VideocamOutline, VideocamOffOutline } from '@vicons/ionicons5' | |||
import { AudioOutlined, AudioMutedOutlined } from '@vicons/antd' | |||
export default { | |||
name: 'HomePage', | |||
components: { | |||
DeviceSelect, | |||
VideocamOutline, | |||
VideocamOffOutline, | |||
AudioOutlined, | |||
AudioMutedOutlined | |||
}, | |||
setup() { | |||
const route = useRoute() | |||
const data = reactive({ | |||
client: null, | |||
sdkAppId: 1400752641, | |||
sdkSecret: '9b5fc557f286d7e4d6eafd8023026da59f0674000f319754aa1ec4beefddcdd6', | |||
userId: '', | |||
roomId: null, | |||
secret: { | |||
'haoran': 'eJwtzEELgjAYxvHvsqsh29ymCB28hIIElZR1G2y1ty2VJSVE3z1Tj8-vgf8HVeUhfGmPUkRDjFbTBqWbHq4wsZGtl83yPJWVXQcKpYRhHHMqGJkfPXTg9eicc4oxnrWHx9*EEFHECGVLBW5jWMn6HCRBW7njJd*WNgNrNrnbFXcZG356V94Vg7W12ydr9P0BbGAyOQ__', | |||
'wanghaoran': 'eJwtzEELgjAcBfDvsnO4ubnFhA6ihwIhUqEOXhZO*6dNUalF9N0z9fh*7-E*KItT56l75CPqELSZMxTajFDCzC9lqptqe2XWdihq1XVQIN-1CNlyKjx3abTtoNeTc84pIWTRER5-E0IwVwq5bgeopnPPRDluaVPGTb1P3nd7CcPMHgJ2zbFJ5HCKrDkHxzLHMmX1Dn1-G3A0Zg__' | |||
} | |||
}) | |||
const settings = reactive({ | |||
localStream: null, // 本地流 | |||
cameraId: null, // 摄像头id | |||
microphoneId: null, // 麦克风id | |||
isPlayingLocalStream: false, // 是否播放本地流 | |||
isMutedVideo: false, // 是否关闭视频 | |||
isMutedAudio: false // 是否关闭音频 | |||
}) | |||
const createMeetingRoom = async() => { | |||
data.client = TRTC.createClient({ | |||
sdkAppId: data.sdkAppId, // 填写您申请的 sdkAppId | |||
userId: data.userId, // 填写您业务对应的 userId | |||
userSig: data.secret[data.userId], // 填写服务器或本地计算的 userSig | |||
mode: 'rtc' | |||
}) | |||
} | |||
/** | |||
* @description: 初始化本地流 | |||
* @return {*} | |||
*/ | |||
const initLocalStream = async() => { | |||
settings.localStream = TRTC.createStream({ | |||
audio: true, | |||
video: true, | |||
userId: data.userId, | |||
cameraId: settings.cameraId, | |||
microphoneId: settings.microphoneId | |||
}) | |||
try { | |||
await settings.localStream.initialize() | |||
} catch (error) { | |||
settings.localStream = null | |||
} | |||
} | |||
/** | |||
* @description: 播放本地流 | |||
* @return {*} | |||
*/ | |||
const playLocalStream = async() => { | |||
settings.localStream.play('localStream') | |||
.then(() => { | |||
settings.isPlayingLocalStream = true | |||
}) | |||
.catch((error) => { | |||
console.error(error) | |||
}) | |||
} | |||
/** | |||
* @description: 打开摄像头 | |||
* @return {*} | |||
*/ | |||
const handleVideoMute = async() => { | |||
if (settings.localStream) { | |||
settings.localStream.muteVideo() | |||
settings.isMutedVideo = true | |||
} | |||
} | |||
/** | |||
* @description: 关闭摄像头 | |||
* @return {*} | |||
*/ | |||
const handleVideoUnMute = async() => { | |||
if (settings.localStream) { | |||
settings.localStream.unmuteVideo() | |||
settings.isMutedVideo = false | |||
} | |||
} | |||
/** | |||
* @description: 打开麦克风 | |||
* @return {*} | |||
*/ | |||
const handleAudioMute = async() => { | |||
if (settings.localStream) { | |||
settings.localStream.muteAudio() | |||
settings.isMutedAudio = true | |||
} | |||
} | |||
/** | |||
* @description: 关闭麦克风 | |||
* @return {*} | |||
*/ | |||
const handleAudioUnMute = async() => { | |||
if (settings.localStream) { | |||
settings.localStream.unmuteAudio() | |||
settings.isMutedAudio = false | |||
} | |||
} | |||
const leaveMeetingRoom = async() => { | |||
await data.client.leave() | |||
} | |||
onMounted(() => { | |||
const { userId, roomId } = route.query | |||
data.userId = userId | |||
data.roomId = Number(roomId) | |||
// nextTick(async() => { | |||
// await createMeetingRoom() | |||
// await initLocalStream() | |||
// }) | |||
}) | |||
watch(() => [settings.cameraId, settings.microphoneId], async([cameraId, microphoneId]) => { | |||
if (cameraId && microphoneId) { | |||
await createMeetingRoom() | |||
await initLocalStream() | |||
await playLocalStream() | |||
} | |||
}) | |||
return { | |||
...toRefs(data), | |||
...toRefs(settings), | |||
handleVideoMute, | |||
handleVideoUnMute, | |||
handleAudioMute, | |||
handleAudioUnMute | |||
} | |||
} | |||
} | |||
</script> | |||
<style scoped lang='scss'> | |||
.room{ | |||
width: 100vw; | |||
height: 100vh; | |||
.local-stream-container{ | |||
width: 100%; | |||
height: 100%; | |||
position: relative; | |||
.local-stream-content { | |||
width: 100%; | |||
height: 100%; | |||
} | |||
.local-stream-control { | |||
width: 100%; | |||
height: 100%; | |||
height: 40px; | |||
position: absolute; | |||
bottom: 5px; | |||
z-index: 99; | |||
display: flex; | |||
justify-content: flex-end; | |||
align-items: center; | |||
padding: 0 10px; | |||
background: rgba(0, 0, 0, 0.3); | |||
.control { | |||
margin-left: 10px; | |||
} | |||
} | |||
} | |||
} | |||
</style> |
@@ -0,0 +1,61 @@ | |||
<template> | |||
<div class="login"> | |||
<n-form ref="formRef" class="login__form" :model="form"> | |||
<n-form-item path="userId"> | |||
<n-input v-model:value="form.userId" placeholder="请输入帐号" /> | |||
</n-form-item> | |||
<n-form-item path="roomId"> | |||
<n-input v-model:value="form.roomId" placeholder="请输入房间号" /> | |||
</n-form-item> | |||
<n-button @click="handleEnterRoom">加入房间</n-button> | |||
</n-form> | |||
</div> | |||
</template> | |||
<script> | |||
import { useRouter } from 'vue-router' | |||
import { ref, reactive, toRefs } from 'vue' | |||
export default { | |||
name: 'LoginPage', | |||
setup() { | |||
const router = useRouter() | |||
const formRef = ref() | |||
const data = reactive({ | |||
form: { | |||
userId: 'wanghaoran', | |||
roomId: 111 | |||
} | |||
}) | |||
const handleEnterRoom = () => { | |||
router.push({ path: '/home', query: { ...data.form }}) | |||
} | |||
return { | |||
...toRefs(data), | |||
formRef, | |||
handleEnterRoom | |||
} | |||
} | |||
} | |||
</script> | |||
<style scoped lang='scss'> | |||
.login{ | |||
width: 100vw; | |||
height: 100vh; | |||
background: #010101; | |||
.login__form{ | |||
width: 80%; | |||
text-align: center; | |||
position: relative; | |||
top: 40%; | |||
left: 50%; | |||
transform: translate(-50%, -50%); | |||
.n-button{ | |||
color: #ffffff; | |||
background-image: linear-gradient(-45deg,#006EFF 0%,#0C59F2 100%); | |||
} | |||
} | |||
} | |||
</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,47 @@ | |||
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 | |||
} | |||
} | |||
}) |