Browse Source

init

develop
zhangtao 1 year ago
commit
bed392944c
72 changed files with 2856 additions and 0 deletions
  1. +7
    -0
      .env
  2. +11
    -0
      .env.development
  3. +14
    -0
      .env.localhost
  4. +11
    -0
      .env.production
  5. +11
    -0
      .env.test
  6. +329
    -0
      .eslintrc.js
  7. +5
    -0
      .gitignore
  8. +3
    -0
      .vscode/extensions.json
  9. +7
    -0
      README.md
  10. +3
    -0
      build/constant.js
  11. +14
    -0
      build/script/build-cname.js
  12. +29
    -0
      build/script/build-config.js
  13. +16
    -0
      build/script/index.js
  14. +71
    -0
      build/utils.js
  15. +33
    -0
      build/vite/plugin/html.js
  16. +25
    -0
      build/vite/plugin/index.js
  17. +14
    -0
      build/vite/plugin/mock.js
  18. +9
    -0
      build/vite/plugin/unocss.js
  19. +17
    -0
      build/vite/proxy.js
  20. +18
    -0
      index.html
  21. +14
    -0
      mock/_create-prod-server.js
  22. +12
    -0
      mock/_utils.js
  23. +47
    -0
      package.json
  24. +1
    -0
      public/vite.svg
  25. +33
    -0
      src/App.vue
  26. +1
    -0
      src/assets/vue.svg
  27. +22
    -0
      src/components/Bread/index.vue
  28. +54
    -0
      src/components/Dialog/index.vue
  29. +81
    -0
      src/components/HelloWorld.vue
  30. +17
    -0
      src/components/LoadingBar/index.vue
  31. +65
    -0
      src/components/Message/index.vue
  32. +66
    -0
      src/layout/components/Header/index.vue
  33. +33
    -0
      src/layout/components/Sidebar/index.vue
  34. +40
    -0
      src/layout/index.vue
  35. +19
    -0
      src/main.js
  36. +9
    -0
      src/router/guard/index.js
  37. +15
    -0
      src/router/guard/page-loading-guard.js
  38. +12
    -0
      src/router/guard/page-title-guard.js
  39. +57
    -0
      src/router/guard/permission-guard.js
  40. +24
    -0
      src/router/index.js
  41. +76
    -0
      src/router/routes/index.js
  42. +4
    -0
      src/router/routes/modules/system.js
  43. +8
    -0
      src/store/index.js
  44. +17
    -0
      src/store/modules/app.js
  45. +30
    -0
      src/store/modules/menus.js
  46. +123
    -0
      src/store/modules/permission.js
  47. +25
    -0
      src/store/modules/setting.js
  48. +56
    -0
      src/store/modules/tagsMenu.js
  49. +53
    -0
      src/store/modules/user.js
  50. +90
    -0
      src/style.css
  51. +3
    -0
      src/styles/index.scss
  52. +32
    -0
      src/styles/layout.scss
  53. +57
    -0
      src/styles/public.scss
  54. +40
    -0
      src/styles/reset.scss
  55. +5
    -0
      src/styles/variables.scss
  56. +9
    -0
      src/utils/cache/index.js
  57. +55
    -0
      src/utils/cache/web-storage.js
  58. +22
    -0
      src/utils/dictionary.js
  59. +17
    -0
      src/utils/http/help.js
  60. +16
    -0
      src/utils/http/index.js
  61. +76
    -0
      src/utils/http/interceptors.js
  62. +85
    -0
      src/utils/index.js
  63. +110
    -0
      src/utils/is.js
  64. +17
    -0
      src/utils/module.js
  65. +40
    -0
      src/utils/token.js
  66. +7
    -0
      src/utils/ui/individuation.js
  67. +56
    -0
      src/utils/ui/theme.js
  68. +109
    -0
      src/views/home/components/Device.vue
  69. +221
    -0
      src/views/home/index.vue
  70. +61
    -0
      src/views/login/index.vue
  71. +20
    -0
      src/views/redirect/index.vue
  72. +47
    -0
      vite.config.js

+ 7
- 0
.env View File

@@ -0,0 +1,7 @@
# title
VITE_APP_TITLE = 'h5'

# 端口号
VITE_PORT = 3000

VITE_SERVER = "/"

+ 11
- 0
.env.development View File

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

+ 14
- 0
.env.localhost View File

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

+ 11
- 0
.env.production View File

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

+ 11
- 0
.env.test View File

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

+ 329
- 0
.eslintrc.js View File

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


+ 5
- 0
.gitignore View File

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

+ 3
- 0
.vscode/extensions.json View File

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

+ 7
- 0
README.md View File

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

+ 3
- 0
build/constant.js View File

@@ -0,0 +1,3 @@
export const GLOB_CONFIG_FILE_NAME = 'app.config.js'
export const GLOB_CONFIG_NAME = '__APP__GLOB__CONF__'
export const OUTPUT_DIR = 'dist'

+ 14
- 0
build/script/build-cname.js View File

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

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

+ 29
- 0
build/script/build-config.js View File

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

function createConfig(option) {
const { config, configName, configFileName } = option
try {
const windowConf = `window.${configName}`
const configStr = `${windowConf}=${JSON.stringify(config)};
Object.freeze(${windowConf});
Object.defineProperty(window, "${configName}", {
configurable: false,
writable: false,
});
`.replace(/\s/g, '')
fs.mkdirp(getRootPath(OUTPUT_DIR))
writeFileSync(getRootPath(`${OUTPUT_DIR}/${configFileName}`), configStr)
} catch (error) {
console.log(chalk.red('configuration file configuration file failed to package:\n' + error))
}
}

export function runBuildConfig() {
const config = getEnvConfig()
const configName = GLOB_CONFIG_NAME
const configFileName = GLOB_CONFIG_FILE_NAME
createConfig({ config, configName, configFileName })
}

+ 16
- 0
build/script/index.js View File

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

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

runBuild()

+ 71
- 0
build/utils.js View File

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

+ 33
- 0
build/vite/plugin/html.js View File

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

+ 25
- 0
build/vite/plugin/index.js View File

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

+ 14
- 0
build/vite/plugin/mock.js View File

@@ -0,0 +1,14 @@
import { viteMockServe } from 'vite-plugin-mock'

export function configMockPlugin(isBuild) {
return viteMockServe({
ignore: /^\_/,
mockPath: 'mock',
localEnabled: !isBuild,
prodEnabled: isBuild,
injectCode: `
import { setupProdMockServer } from '../mock/_create-prod-server';
setupProdMockServer();
`,
})
}

+ 9
- 0
build/vite/plugin/unocss.js View File

@@ -0,0 +1,9 @@
import Unocss from 'unocss/vite'
import { presetUno, presetAttributify, presetIcons } from 'unocss'

// https://github.com/antfu/unocss
export function unocss() {
return Unocss({
presets: [presetUno(), presetAttributify(), presetIcons()]
})
}

+ 17
- 0
build/vite/proxy.js View File

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

+ 18
- 0
index.html View File

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

+ 14
- 0
mock/_create-prod-server.js View File

@@ -0,0 +1,14 @@
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'

const modules = import.meta.globEager('./**/*.js')
const mockModules = []
Object.keys(modules).forEach((key) => {
if (key.includes('/_')) {
return
}
mockModules.push(...modules[key].default)
})

export function setupProdMockServer() {
createProdMockServer(mockModules)
}

+ 12
- 0
mock/_utils.js View File

@@ -0,0 +1,12 @@
export function resolveToken(authorization) {
/**
* * jwt token
* * Bearer + token
* ! 认证方案: Bearer
*/
const reqTokenSplit = authorization.split(' ')
if (reqTokenSplit.length === 2) {
return reqTokenSplit[1]
}
return ''
}

+ 47
- 0
package.json View File

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

+ 1
- 0
public/vite.svg View File

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

+ 33
- 0
src/App.vue View File

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

+ 1
- 0
src/assets/vue.svg View File

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

+ 22
- 0
src/components/Bread/index.vue View File

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

+ 54
- 0
src/components/Dialog/index.vue View File

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


+ 81
- 0
src/components/HelloWorld.vue View File

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

+ 17
- 0
src/components/LoadingBar/index.vue View File

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


+ 65
- 0
src/components/Message/index.vue View File

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

+ 66
- 0
src/layout/components/Header/index.vue View File

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

+ 33
- 0
src/layout/components/Sidebar/index.vue View File

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

+ 40
- 0
src/layout/index.vue View File

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

+ 19
- 0
src/main.js View File

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

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

import App from './App.vue'

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

setupStore(app)
setupRouter(app)

app.mount('#app')
}

setupApp()

+ 9
- 0
src/router/guard/index.js View File

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

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

+ 15
- 0
src/router/guard/page-loading-guard.js View File

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

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

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

+ 12
- 0
src/router/guard/page-title-guard.js View File

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

+ 57
- 0
src/router/guard/permission-guard.js View File

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

+ 24
- 0
src/router/index.js View File

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

+ 76
- 0
src/router/routes/index.js View File

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

+ 4
- 0
src/router/routes/modules/system.js View File

@@ -0,0 +1,4 @@
export default [

]


+ 8
- 0
src/store/index.js View File

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

+ 17
- 0
src/store/modules/app.js View File

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

export const useAppStore = defineStore('app', {
state() {
return {
themeOverrides: {
common: {
primaryColor: '#316c72',
primaryColorSuppl: '#316c72',
primaryColorHover: '#316c72',
successColorHover: '#316c72',
successColorSuppl: '#316c72'
}
}
}
}
})

+ 30
- 0
src/store/modules/menus.js View File

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

+ 123
- 0
src/store/modules/permission.js View File

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

+ 25
- 0
src/store/modules/setting.js View File

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

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

+ 56
- 0
src/store/modules/tagsMenu.js View File

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

+ 53
- 0
src/store/modules/user.js View File

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

+ 90
- 0
src/style.css View File

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

+ 3
- 0
src/styles/index.scss View File

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

+ 32
- 0
src/styles/layout.scss View File

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

+ 57
- 0
src/styles/public.scss View File

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

+ 40
- 0
src/styles/reset.scss View File

@@ -0,0 +1,40 @@
html {
box-sizing: border-box;
}

*,
::before,
::after {
margin: 0;
padding: 0;
box-sizing: inherit;
}

a {
text-decoration: none;
color: #333;
}

a:hover,
a:link,
a:visited,
a:active {
text-decoration: none;
}

ol,
ul {
list-style: none;
}

input,
textarea {
outline: none;
border: none;
resize: none;
}

body {
font-size: 14px;
font-weight: 400;
}

+ 5
- 0
src/styles/variables.scss View File

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

:root {
--vh100: 100vh;
}

+ 9
- 0
src/utils/cache/index.js View File

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

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

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

+ 55
- 0
src/utils/cache/web-storage.js View File

@@ -0,0 +1,55 @@
import { isNullOrUndef } from '@/utils/is'

class WebStorage {
constructor(option) {
this.storage = option.storage
this.prefixKey = option.prefixKey
}

getKey(key) {
return `${this.prefixKey}${key}`.toUpperCase()
}

set(key, value, expire) {
const stringData = JSON.stringify({
value,
time: Date.now(),
expire: !isNullOrUndef(expire) ? new Date().getTime() + expire * 1000 : null
})
this.storage.setItem(this.getKey(key), stringData)
}

get(key) {
const { value } = this.getItem(key, {})
return value
}

getItem(key, def = null) {
const val = this.storage.getItem(this.getKey(key))
if (!val) return def
try {
const data = JSON.parse(val)
const { value, time, expire } = data
if (isNullOrUndef(expire) || expire > new Date().getTime()) {
return { value, time }
}
this.remove(key)
return def
} catch (error) {
this.remove(key)
return def
}
}

remove(key) {
this.storage.removeItem(this.getKey(key))
}

clear() {
this.storage.clear()
}
}

export function createWebStorage({ prefixKey = '', storage = sessionStorage }) {
return new WebStorage({ prefixKey, storage })
}

+ 22
- 0
src/utils/dictionary.js View File

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

+ 17
- 0
src/utils/http/help.js View File

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

+ 16
- 0
src/utils/http/index.js View File

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

+ 76
- 0
src/utils/http/interceptors.js View File

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

+ 85
- 0
src/utils/index.js View File

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

+ 110
- 0
src/utils/is.js View File

@@ -0,0 +1,110 @@
const toString = Object.prototype.toString

export function is(val, type) {
return toString.call(val) === `[object ${type}]`
}

export function isDef(val) {
return typeof val !== 'undefined'
}

export function isUndef(val) {
return typeof val === 'undefined'
}

export function isNull(val) {
return val === null
}

export function isObject(val) {
return !isNull(val) && is(val, 'Object')
}

export function isArray(val) {
return val && Array.isArray(val)
}

export function isString(val) {
return is(val, 'String')
}

export function isNumber(val) {
return is(val, 'Number')
}

export function isBoolean(val) {
return is(val, 'Boolean')
}

export function isDate(val) {
return is(val, 'Date')
}

export function isRegExp(val) {
return is(val, 'RegExp')
}

export function isFunction(val) {
return typeof val === 'function'
}

export function isPromise(val) {
return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch)
}

export function isElement(val) {
return isObject(val) && !!val.tagName
}

export function isWindow(val) {
return typeof window !== 'undefined' && isDef(window) && is(val, 'Window')
}

export function isNullOrUndef(val) {
return isNull(val) || isUndef(val)
}

export function isEmpty(val) {
if (isArray(val) || isString(val)) {
return val.length === 0
}

if (val instanceof Map || val instanceof Set) {
return val.size === 0
}

if (isObject(val)) {
return Object.keys(val).length === 0
}

return false
}

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

export function isUrl(path) {
const reg =
/(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/
return reg.test(path)
}

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

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

export const isClient = !isServer

+ 17
- 0
src/utils/module.js View File

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

+ 40
- 0
src/utils/token.js View File

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

+ 7
- 0
src/utils/ui/individuation.js View File

@@ -0,0 +1,7 @@
export const Button = {
/* 按钮的颜色 */
color: 'rgba(56, 174, 243, 1)',
/* 按钮文本颜色 */
textColor: 'rgba(56, 174, 243, 1)'
}


+ 56
- 0
src/utils/ui/theme.js View File

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

+ 109
- 0
src/views/home/components/Device.vue View File

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

+ 221
- 0
src/views/home/index.vue View File

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

+ 61
- 0
src/views/login/index.vue View File

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

+ 20
- 0
src/views/redirect/index.vue View File

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

+ 47
- 0
vite.config.js View File

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

Loading…
Cancel
Save