init
This commit is contained in:
commit
3d75632663
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
end_of_line = auto
|
6
.env.development
Normal file
6
.env.development
Normal file
@ -0,0 +1,6 @@
|
||||
# 开发环境
|
||||
VITE_PORT = 9000
|
||||
VITE_APP_NAME = 'digital-agriculture-screen'
|
||||
VITE_APP_NAME = '政务云数据大屏可视化'
|
||||
VITE_APP_BASE_API = '/apis'
|
||||
VITE_APP_BASE_URL = 'http://192.168.18.99:8080'
|
5
.env.production
Normal file
5
.env.production
Normal file
@ -0,0 +1,5 @@
|
||||
# 生产环境
|
||||
VITE_APP_NAME = 'digital-agriculture-screen'
|
||||
VITE_APP_NAME = '政务云数据大屏可视化'
|
||||
VITE_APP_BASE_API = '/apis'
|
||||
VITE_APP_BASE_URL = ''
|
5
.env.test
Normal file
5
.env.test
Normal file
@ -0,0 +1,5 @@
|
||||
# 测试环境
|
||||
VITE_APP_NAME = 'digital-agriculture-screen'
|
||||
VITE_APP_NAME = '政务云数据大屏可视化'
|
||||
VITE_APP_BASE_API = '/apis'
|
||||
VITE_APP_BASE_URL = ''
|
14
.eslintignore
Normal file
14
.eslintignore
Normal file
@ -0,0 +1,14 @@
|
||||
*.sh
|
||||
*.md
|
||||
*.woff
|
||||
*.ttf
|
||||
.vscode
|
||||
.idea
|
||||
.husky
|
||||
.local
|
||||
dist
|
||||
src/assets
|
||||
node_modules
|
||||
Dockerfile
|
||||
stats.html
|
||||
tailwind.config.js
|
62
.eslintrc.cjs
Normal file
62
.eslintrc.cjs
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* @Descripttion: .eslintrc.cjs
|
||||
* 在VSCode中安装ESLint插件,编写过程中检测代码质量
|
||||
* ESLint 代码质量校验相关配置
|
||||
* 这里使用prettier作为代码格式化工具,用ESLint做代码质检
|
||||
* 相关配置使用下面extends扩展先做默认设置
|
||||
* 在.prettierrc.cjs文件中配置好后,格式化规则会以.prettierrc.cjs作为最终格式,所以不建议在本文件中做代码格式化相关配置
|
||||
* 相关prettier配置ESLint会默认加载为代码质检 格式化以prettier为主
|
||||
* 在本配置文件中只做代码质量约束规范配置
|
||||
* @Author: zenghua.wang
|
||||
* @Date: 2022-09-22 15:53:58
|
||||
* @LastEditors: zenghua.wang
|
||||
* @LastEditTime: 2024-03-22 10:19:39
|
||||
*/
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint-config-prettier',
|
||||
'eslint:recommended',
|
||||
// 'plugin:@typescript-eslint/recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:vue/vue3-essential',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
files: ['.eslintrc.{js,cjs}'],
|
||||
parserOptions: {
|
||||
sourceType: 'script',
|
||||
},
|
||||
},
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
requireConfigFile: false,
|
||||
parser: '@babel/eslint-parser',
|
||||
// parser: '@typescript-eslint/parser',
|
||||
},
|
||||
plugins: ['vue', 'prettier'],
|
||||
globals: {
|
||||
defineProps: 'readonly',
|
||||
defineEmits: 'readonly',
|
||||
defineExpose: 'readonly',
|
||||
withDefaults: 'readonly',
|
||||
},
|
||||
// 这里时配置规则的,自己看情况配置
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
'no-debugger': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'vue/no-unused-vars': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
},
|
||||
};
|
115
.gitignore
vendored
Normal file
115
.gitignore
vendored
Normal file
@ -0,0 +1,115 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
# .env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
20
.prettierignore
Normal file
20
.prettierignore
Normal file
@ -0,0 +1,20 @@
|
||||
## OS
|
||||
.DS_Store
|
||||
node_modules
|
||||
.idea
|
||||
.editorconfig
|
||||
package-lock.json
|
||||
.npmrc
|
||||
|
||||
# Ignored suffix
|
||||
*.log
|
||||
*.md
|
||||
*.svg
|
||||
*.png
|
||||
*ignore
|
||||
|
||||
## Local
|
||||
|
||||
## Built-files
|
||||
.cache
|
||||
dist
|
52
.prettierrc.cjs
Normal file
52
.prettierrc.cjs
Normal file
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* @Descripttion: .prettierrc.cjs
|
||||
* 在VSCode中安装prettier插件 打开插件配置填写`.prettierrc.js` 将本文件作为其代码格式化规范
|
||||
* 在本文件中修改格式化规则,不会同时触发改变ESLint代码检查,所以每次修改本文件需要重启VSCode,ESLint检查才能同步代码格式化
|
||||
* 需要相应的代码格式化规范请自行查阅配置,下面为默认项目配置
|
||||
* @Author: zenghua.wang
|
||||
* @Date: 2022-09-22 15:53:58
|
||||
* @LastEditors: zenghua.wang
|
||||
* @LastEditTime: 2024-01-24 19:22:25
|
||||
*/
|
||||
module.exports = {
|
||||
// 一行最多多少个字符
|
||||
printWidth: 150,
|
||||
// 指定每个缩进级别的空格数
|
||||
tabWidth: 2,
|
||||
// 使用制表符而不是空格缩进行
|
||||
useTabs: false,
|
||||
// 在语句末尾是否需要分号
|
||||
semi: true,
|
||||
// 是否使用单引号
|
||||
singleQuote: true,
|
||||
// 更改引用对象属性的时间 可选值"<as-needed|consistent|preserve>"
|
||||
quoteProps: 'as-needed',
|
||||
// 在JSX中使用单引号而不是双引号
|
||||
jsxSingleQuote: false,
|
||||
// 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"<none|es5|all>",默认none
|
||||
trailingComma: 'es5',
|
||||
// 在对象文字中的括号之间打印空格
|
||||
bracketSpacing: true,
|
||||
// jsx 标签的反尖括号需要换行
|
||||
jsxBracketSameLine: false,
|
||||
// 在单独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x
|
||||
arrowParens: 'always',
|
||||
// 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码
|
||||
rangeStart: 0,
|
||||
rangeEnd: Infinity,
|
||||
// 指定要使用的解析器,不需要写文件开头的 @prettier
|
||||
requirePragma: false,
|
||||
// 不需要自动在文件开头插入 @prettier
|
||||
insertPragma: false,
|
||||
// 使用默认的折行标准 always\never\preserve
|
||||
proseWrap: 'preserve',
|
||||
// 指定HTML文件的全局空格敏感度 css\strict\ignore
|
||||
htmlWhitespaceSensitivity: 'css',
|
||||
// Vue文件脚本和样式标签缩进
|
||||
vueIndentScriptAndStyle: false,
|
||||
//在 windows 操作系统中换行符通常是回车 (CR) 加换行分隔符 (LF),也就是回车换行(CRLF),
|
||||
//然而在 Linux 和 Unix 中只使用简单的换行分隔符 (LF)。
|
||||
//对应的控制字符为 "\n" (LF) 和 "\r\n"(CRLF)。auto意为保持现有的行尾
|
||||
// 换行符使用 lf 结尾是 可选值"<auto|lf|crlf|cr>"
|
||||
endOfLine: 'auto',
|
||||
};
|
17
.stylelintignore
Normal file
17
.stylelintignore
Normal file
@ -0,0 +1,17 @@
|
||||
# .stylelintignore
|
||||
# 旧的不需打包的样式库
|
||||
*.min.css
|
||||
|
||||
# 其他类型文件
|
||||
*.js
|
||||
*.jpg
|
||||
*.png
|
||||
*.eot
|
||||
*.ttf
|
||||
*.woff
|
||||
*.json
|
||||
|
||||
# 测试和打包目录
|
||||
/dist/*
|
||||
/node_modules/*
|
||||
/src/assets/*
|
131
.stylelintrc.cjs
Normal file
131
.stylelintrc.cjs
Normal file
@ -0,0 +1,131 @@
|
||||
/*
|
||||
* @Descripttion: .stylelintrc.cjs
|
||||
* @Author: zenghua.wang
|
||||
* @Date: 2022-09-22 15:53:58
|
||||
* @LastEditors: zenghua.wang
|
||||
* @LastEditTime: 2024-01-24 18:49:26
|
||||
*/
|
||||
module.exports = {
|
||||
root: true,
|
||||
plugins: ['stylelint-order', 'stylelint-scss'],
|
||||
extends: [
|
||||
'stylelint-config-standard',
|
||||
'stylelint-config-standard-scss',
|
||||
'stylelint-config-prettier',
|
||||
'stylelint-config-html/vue',
|
||||
'stylelint-config-recommended-vue',
|
||||
'stylelint-config-recommended-scss'
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.{html,vue}'],
|
||||
customSyntax: 'postcss-html'
|
||||
}
|
||||
],
|
||||
rules: {
|
||||
indentation: 2,
|
||||
'selector-pseudo-element-no-unknown': [
|
||||
true,
|
||||
{
|
||||
ignorePseudoElements: ['v-deep', ':deep']
|
||||
}
|
||||
],
|
||||
'number-leading-zero': 'always',
|
||||
'no-descending-specificity': null,
|
||||
'function-url-quotes': 'always',
|
||||
'string-quotes': 'single',
|
||||
'unit-case': null,
|
||||
'color-hex-case': 'lower',
|
||||
'color-hex-length': 'long',
|
||||
'rule-empty-line-before': 'never',
|
||||
'font-family-no-missing-generic-family-keyword': null,
|
||||
'selector-type-no-unknown': null,
|
||||
'block-opening-brace-space-before': 'always',
|
||||
'at-rule-no-unknown': null,
|
||||
'no-duplicate-selectors': null,
|
||||
'property-no-unknown': null,
|
||||
'no-empty-source': null,
|
||||
'selector-class-pattern': null,
|
||||
'keyframes-name-pattern': null,
|
||||
'selector-pseudo-class-no-unknown': [true, { ignorePseudoClasses: ['global', 'deep'] }],
|
||||
'function-no-unknown': null,
|
||||
'order/properties-order': [
|
||||
'position',
|
||||
'top',
|
||||
'right',
|
||||
'bottom',
|
||||
'left',
|
||||
'z-index',
|
||||
'display',
|
||||
'justify-content',
|
||||
'align-items',
|
||||
'float',
|
||||
'clear',
|
||||
'overflow',
|
||||
'overflow-x',
|
||||
'overflow-y',
|
||||
'margin',
|
||||
'margin-top',
|
||||
'margin-right',
|
||||
'margin-bottom',
|
||||
'margin-left',
|
||||
'padding',
|
||||
'padding-top',
|
||||
'padding-right',
|
||||
'padding-bottom',
|
||||
'padding-left',
|
||||
'width',
|
||||
'min-width',
|
||||
'max-width',
|
||||
'height',
|
||||
'min-height',
|
||||
'max-height',
|
||||
'font-size',
|
||||
'font-family',
|
||||
'font-weight',
|
||||
'border',
|
||||
'border-style',
|
||||
'border-width',
|
||||
'border-color',
|
||||
'border-top',
|
||||
'border-top-style',
|
||||
'border-top-width',
|
||||
'border-top-color',
|
||||
'border-right',
|
||||
'border-right-style',
|
||||
'border-right-width',
|
||||
'border-right-color',
|
||||
'border-bottom',
|
||||
'border-bottom-style',
|
||||
'border-bottom-width',
|
||||
'border-bottom-color',
|
||||
'border-left',
|
||||
'border-left-style',
|
||||
'border-left-width',
|
||||
'border-left-color',
|
||||
'border-radius',
|
||||
'text-align',
|
||||
'text-justify',
|
||||
'text-indent',
|
||||
'text-overflow',
|
||||
'text-decoration',
|
||||
'white-space',
|
||||
'color',
|
||||
'background',
|
||||
'background-position',
|
||||
'background-repeat',
|
||||
'background-size',
|
||||
'background-color',
|
||||
'background-clip',
|
||||
'opacity',
|
||||
'filter',
|
||||
'list-style',
|
||||
'outline',
|
||||
'visibility',
|
||||
'box-shadow',
|
||||
'text-shadow',
|
||||
'resize',
|
||||
'transition'
|
||||
]
|
||||
}
|
||||
};
|
75
auto-imports.d.ts
vendored
Normal file
75
auto-imports.d.ts
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useLink: typeof import('vue-router')['useLink']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
38
components.d.ts
vendored
Normal file
38
components.d.ts
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Components: typeof import('./src/components/index.js')['default']
|
||||
CustomCarouselPicture: typeof import('./src/components/custom-carousel-picture/index.vue')['default']
|
||||
CustomEchartBar: typeof import('./src/components/custom-echart-bar/index.vue')['default']
|
||||
CustomEchartBubble: typeof import('./src/components/custom-echart-bubble/index.vue')['default']
|
||||
CustomEchartLine: typeof import('./src/components/custom-echart-line/index.vue')['default']
|
||||
CustomEchartLineLine: typeof import('./src/components/custom-echart-line-line/index.vue')['default']
|
||||
CustomEchartMaps: typeof import('./src/components/custom-echart-maps/index.vue')['default']
|
||||
CustomEchartMixin: typeof import('./src/components/custom-echart-mixin/index.vue')['default']
|
||||
CustomEchartPictorialBar: typeof import('./src/components/custom-echart-pictorial-bar/index.vue')['default']
|
||||
CustomEchartPie: typeof import('./src/components/custom-echart-pie/index.vue')['default']
|
||||
CustomEchartPie3d: typeof import('./src/components/custom-echart-pie-3d/index.vue')['default']
|
||||
CustomEchartPieGauge: typeof import('./src/components/custom-echart-pie-gauge/index.vue')['default']
|
||||
CustomEchartRadar: typeof import('./src/components/custom-echart-radar/index.vue')['default']
|
||||
CustomEchartScatterBlister: typeof import('./src/components/custom-echart-scatter-blister/index.vue')['default']
|
||||
CustomEchartWaterDroplet: typeof import('./src/components/custom-echart-water-droplet/index.vue')['default']
|
||||
CustomEchartWordCloud: typeof import('./src/components/custom-echart-word-cloud/index.vue')['default']
|
||||
CustomIframe: typeof import('./src/components/custom-iframe/index.vue')['default']
|
||||
CustomImportExcel: typeof import('./src/components/custom-import-excel/index.vue')['default']
|
||||
CustomRankList: typeof import('./src/components/custom-rank-list/index.vue')['default']
|
||||
CustomRichEditor: typeof import('./src/components/custom-rich-editor/index.vue')['default']
|
||||
CustomScrollBoard: typeof import('./src/components/custom-scroll-board/index.vue')['default']
|
||||
CustomTableOperate: typeof import('./src/components/custom-table-operate/index.vue')['default']
|
||||
CustomTableTree: typeof import('./src/components/custom-table-tree/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SvgIcon: typeof import('./src/components/svg-icon/index.vue')['default']
|
||||
UpFile: typeof import('./src/components/custom-rich-editor/upFile.js')['default']
|
||||
}
|
||||
}
|
16
index.html
Normal file
16
index.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>数据大屏</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
79
package.json
Normal file
79
package.json
Normal file
@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "digital-agriculture-screen",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --mode development",
|
||||
"build": "vite build --mode production",
|
||||
"test": "vite build --mode test",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write 'src/**/*.{vue,ts,tsx,js,jsx,css,less,scss,json,md}'",
|
||||
"eslint": "npx eslint --init",
|
||||
"lint": "npm run lint:script && npm run lint:style",
|
||||
"lint:style": "stylelint 'src/**/*.{vue,scss,css,sass,less}' --fix",
|
||||
"lint:script": "eslint --ext .js,.ts,.tsx,.vue --fix --quiet ./src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@smallwei/avue": "^3.6.2",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.6.5",
|
||||
"echarts": "^5.6.0",
|
||||
"echarts-gl": "^2.0.9",
|
||||
"element-plus": "^2.7.2",
|
||||
"js-base64": "^3.7.6",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"screenfull": "^6.0.2",
|
||||
"vue": "^3.3.11",
|
||||
"vue-cesium": "^3.2.9",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue3-scroll-seamless": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.23.7",
|
||||
"@babel/eslint-parser": "^7.23.3",
|
||||
"@types/path-browserify": "^1.0.2",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-vue": "^9.20.1",
|
||||
"mockjs": "^1.1.0",
|
||||
"postcss": "^8.4.33",
|
||||
"postcss-html": "^1.6.0",
|
||||
"postcss-import": "^16.0.0",
|
||||
"prettier": "^3.2.4",
|
||||
"sass": "^1.70.0",
|
||||
"stylelint": "^16.2.0",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-prettier": "^9.0.5",
|
||||
"stylelint-config-rational-order": "^0.1.2",
|
||||
"stylelint-config-recommended": "^14.0.0",
|
||||
"stylelint-config-recommended-scss": "^14.0.0",
|
||||
"stylelint-config-recommended-vue": "^1.5.0",
|
||||
"stylelint-config-standard": "^36.0.0",
|
||||
"stylelint-config-standard-scss": "^13.0.0",
|
||||
"stylelint-order": "^6.0.4",
|
||||
"stylelint-scss": "^6.1.0",
|
||||
"terser": "^5.27.0",
|
||||
"unplugin-auto-import": "^0.17.3",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.0.8",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-mock": "^3.0.1",
|
||||
"vite-plugin-progress": "^0.0.7",
|
||||
"vite-plugin-qiankun": "^1.0.15",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-vue-setup-extend": "^0.4.0"
|
||||
}
|
||||
}
|
BIN
public/images/avatar.gif
Normal file
BIN
public/images/avatar.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.2 KiB |
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
21
src/App.vue
Normal file
21
src/App.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<el-config-provider :size="size" :locale="zhCn">
|
||||
<router-view />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup name="app">
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue';
|
||||
import { useSettingStore } from '@/store/modules/setting';
|
||||
// 配置element中文
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
|
||||
const SettingStore = useSettingStore();
|
||||
// 配置全局组件大小
|
||||
const size = computed(() => SettingStore.themeConfig.globalComSize);
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import './styles/style';
|
||||
</style>
|
35
src/apis/index.js
Normal file
35
src/apis/index.js
Normal file
@ -0,0 +1,35 @@
|
||||
// import request from '@/utils/axios';
|
||||
// import { isEmpty } from '@/utils';
|
||||
|
||||
// /**
|
||||
// * @Title: 获取字典
|
||||
// */
|
||||
// export function CommonDicData(params = { pageNum: 1, pageSize: 20, dictType: null }) {
|
||||
// if (isEmpty(params?.dictType)) return;
|
||||
// return request(`/system/dict/data/list`, {
|
||||
// method: 'GET',
|
||||
// apisType: 'dicData',
|
||||
// params,
|
||||
// });
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * @Title: 上传图片
|
||||
// */
|
||||
// export function CommonUpload(data, params) {
|
||||
// return request(`/upload`, {
|
||||
// method: 'POST',
|
||||
// apisType: 'upload',
|
||||
// uploadType: 'multipart/form-data',
|
||||
// data,
|
||||
// params,
|
||||
// });
|
||||
// }
|
||||
|
||||
// //云南省所有区域信息
|
||||
// export function getRegion(code) {
|
||||
// let codeVal = code ? code : '530000';
|
||||
// return request('/system/area/region?areaCode=' + codeVal, {
|
||||
// method: 'GET',
|
||||
// });
|
||||
// }
|
233
src/components/custom-carousel-picture/index.vue
Normal file
233
src/components/custom-carousel-picture/index.vue
Normal file
@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<div class="carousel" style="width: 500px">
|
||||
<el-carousel
|
||||
v-if="type === 'image'"
|
||||
ref="carouselRef"
|
||||
:interval="option.interval"
|
||||
:arrow="option.arrow"
|
||||
:indicator-position="option.indicatorPosition"
|
||||
@change="handleChange"
|
||||
>
|
||||
<el-carousel-item v-for="(item, index) in data" :key="index">
|
||||
<img :src="item.image" style="width: 100%; height: auto" />
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
|
||||
<div v-if="type === 'video'" class="carousel-video">
|
||||
<!-- <img :src="state.videoPicture" class="carousel-video-picture" /> -->
|
||||
<video ref="videoRef" controls class="carousel-video-video" width="100%" height="100%" @ended="pauseVideo">
|
||||
<source :src="state.videoUrl" type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<!-- <span class="carousel-video-btn" @click="handlePlay">
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
</span> -->
|
||||
</div>
|
||||
|
||||
<div class="carousel-container">
|
||||
<span class="carousel-arrow carousel-arrow-left" @click="handleLeftClick">
|
||||
<el-icon><ArrowLeftBold /></el-icon>
|
||||
</span>
|
||||
<el-scrollbar ref="scrollbarRef" class="carousel-list">
|
||||
<div
|
||||
v-for="(item, index) in data"
|
||||
:key="index"
|
||||
:class="`carousel-list-item ${state.current === index ? 'active' : ''}`"
|
||||
@click="handleClick(index)"
|
||||
>
|
||||
<el-image style="width: 100px; height: 100px" :src="item.image ?? item" fit="cover" />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<span class="carousel-arrow carousel-arrow-right" @click="handleRightClick">
|
||||
<el-icon><ArrowRightBold /></el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup name="custom-carousel-picture">
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: { type: Array, default: () => [] },
|
||||
type: { type: String, default: 'image' },
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
height: '',
|
||||
initialIndex: 0,
|
||||
autoplay: true,
|
||||
loop: true,
|
||||
interval: 3000,
|
||||
arrow: 'never',
|
||||
indicatorPosition: 'none',
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['']);
|
||||
|
||||
const carouselRef = ref(null);
|
||||
const scrollbarRef = ref(null);
|
||||
const videoRef = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
current: 0,
|
||||
isReady: false,
|
||||
videoPicture: '',
|
||||
videoUrl: '',
|
||||
});
|
||||
|
||||
const handleChange = (cur, last) => {
|
||||
state.current = cur;
|
||||
};
|
||||
|
||||
const handleLeftClick = () => {
|
||||
// const index = carouselRef.value.activeIndex;
|
||||
// carouselRef.value.setActiveItem(index + 1);
|
||||
scrollbarRef.value.setScrollLeft(scrollbarRef.value.wrapRef.scrollLeft - 120);
|
||||
};
|
||||
|
||||
const handleRightClick = () => {
|
||||
// const index = carouselRef.value.activeIndex;
|
||||
// carouselRef.value.setActiveItem(index - 1);
|
||||
scrollbarRef.value.setScrollLeft(scrollbarRef.value.wrapRef.scrollLeft + 120);
|
||||
};
|
||||
|
||||
const playVideo = () => {
|
||||
if (videoRef.value) {
|
||||
videoRef.value.play();
|
||||
}
|
||||
};
|
||||
|
||||
const pauseVideo = () => {
|
||||
if (videoRef.value) {
|
||||
videoRef.value.pause();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (index) => {
|
||||
const { type, data } = props;
|
||||
switch (type) {
|
||||
case 'image': {
|
||||
carouselRef.value.setActiveItem(index);
|
||||
break;
|
||||
}
|
||||
case 'video': {
|
||||
const url = data[index];
|
||||
state.videoUrl = url;
|
||||
playVideo();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlay = () => {
|
||||
playVideo();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.carousel {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
&-list {
|
||||
flex: 3;
|
||||
|
||||
:deep(.el-scrollbar__bar) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-scrollbar__view) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-item {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin: 10px;
|
||||
border: 2px solid #fff;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
background: var(--el-color-danger-light-9);
|
||||
color: var(--el-color-danger);
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
|
||||
.el-icon {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 0 auto;
|
||||
line-height: 40px;
|
||||
font-size: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&-video {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
|
||||
&-picture {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&-video {
|
||||
position: absolute;
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
&-btn {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
z-index: 12;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin-left: -25px;
|
||||
margin-top: -25px;
|
||||
border-radius: 25px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
cursor: pointer;
|
||||
|
||||
.el-icon {
|
||||
font-size: 32px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
110
src/components/custom-echart-bar/index.vue
Normal file
110
src/components/custom-echart-bar/index.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '../../hooks/useEcharts';
|
||||
export default {
|
||||
name: 'CustomEchartBar',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'bar',
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
isSeries: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
label: {
|
||||
show: true,
|
||||
backgroundColor: '#333',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
top: 30,
|
||||
},
|
||||
grid: {
|
||||
top: 60,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: [],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
let typeArr = Array.from(new Set(props.chartData.map((item) => item.type)));
|
||||
let xAxisData = Array.from(new Set(props.chartData.map((item) => item.name)));
|
||||
let seriesData = [];
|
||||
typeArr.forEach((type, index) => {
|
||||
const barStyle = props.option?.barStyle ?? {};
|
||||
let obj = { name: type, type: props.type, ...barStyle };
|
||||
let data = [];
|
||||
xAxisData.forEach((x) => {
|
||||
let dataArr = props.chartData.filter((item) => type === item.type && item.name == x);
|
||||
if (dataArr && dataArr.length > 0) {
|
||||
data.push(dataArr[0].value);
|
||||
} else {
|
||||
data.push(null);
|
||||
}
|
||||
});
|
||||
obj['data'] = data;
|
||||
if (props.option?.color) {
|
||||
obj.color = props.option?.color[index];
|
||||
}
|
||||
seriesData.push(obj);
|
||||
});
|
||||
option.series = props.isSeries && option.series.length > 0 ? option.series : seriesData;
|
||||
option.xAxis.data = xAxisData;
|
||||
setOptions(option);
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
103
src/components/custom-echart-bubble/index.vue
Normal file
103
src/components/custom-echart-bubble/index.vue
Normal file
@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '../../hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'customEchartBubble',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
formatter: function (params) {
|
||||
console.log(params);
|
||||
var str = params.marker + '' + params.data.name + '</br>' + '交易额:' + params.data.value + '万元</br>';
|
||||
return str;
|
||||
},
|
||||
},
|
||||
animationDurationUpdate: function (idx) {
|
||||
// 越往后的数据延迟越大
|
||||
return idx * 100;
|
||||
},
|
||||
animationEasingUpdate: 'bounceIn',
|
||||
color: ['#fff', '#fff', '#fff'],
|
||||
series: [
|
||||
{
|
||||
type: 'graph',
|
||||
layout: 'force',
|
||||
force: {
|
||||
repulsion: 80,
|
||||
edgeLength: 20,
|
||||
},
|
||||
roam: true,
|
||||
label: {
|
||||
normal: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
data: props.chartData,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
console.info(' option.series[0]', props.chartData);
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.series[0].data = props.chartData;
|
||||
setOptions(option);
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
70
src/components/custom-echart-line-line/index.vue
Normal file
70
src/components/custom-echart-line-line/index.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '../../hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartLineLine',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize } = useEcharts(chartRef);
|
||||
const optionVal = reactive({});
|
||||
|
||||
watchEffect(() => {
|
||||
props.option && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(optionVal, cloneDeep(props.option));
|
||||
}
|
||||
setOptions(props.option);
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
152
src/components/custom-echart-line/index.vue
Normal file
152
src/components/custom-echart-line/index.vue
Normal file
@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '../../hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartLine',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'line',
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
label: {
|
||||
show: true,
|
||||
backgroundColor: '#333',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
top: 30,
|
||||
},
|
||||
grid: {
|
||||
top: 60,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: [],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
function hexToRGBA(hex, alpha = 1) {
|
||||
let hexCode = hex.replace('#', '');
|
||||
if (hexCode.length === 3) {
|
||||
hexCode = hexCode
|
||||
.split('')
|
||||
.map((char) => char + char)
|
||||
.join('');
|
||||
}
|
||||
const r = parseInt(hexCode.slice(0, 2), 16);
|
||||
const g = parseInt(hexCode.slice(2, 4), 16);
|
||||
const b = parseInt(hexCode.slice(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
function setAreaStyle(color) {
|
||||
return {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: hexToRGBA(color, 0.2),
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: hexToRGBA(color, 1),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
let typeArr = Array.from(new Set(props.chartData.map((item) => item.type)));
|
||||
let xAxisData = Array.from(new Set(props.chartData.map((item) => item.name)));
|
||||
let seriesData = [];
|
||||
typeArr.forEach((type, index) => {
|
||||
let obj = {
|
||||
name: type,
|
||||
type: props.type,
|
||||
smooth: true,
|
||||
};
|
||||
if (props.option?.color) {
|
||||
obj.areaStyle = setAreaStyle(props.option?.color[index]);
|
||||
}
|
||||
const findItem = props.chartData.find((item) => item.type == type);
|
||||
if (findItem && findItem.color) {
|
||||
obj.color = findItem.color;
|
||||
obj.areaStyle = setAreaStyle(findItem.color[index]);
|
||||
}
|
||||
let data = [];
|
||||
xAxisData.forEach((x) => {
|
||||
let dataArr = props.chartData.filter((item) => type === item.type && item.name == x);
|
||||
if (dataArr && dataArr.length > 0) {
|
||||
data.push(dataArr[0].value);
|
||||
} else {
|
||||
data.push(null);
|
||||
}
|
||||
});
|
||||
obj['data'] = data;
|
||||
seriesData.push(obj);
|
||||
});
|
||||
option.series = seriesData;
|
||||
option.xAxis.data = xAxisData;
|
||||
setOptions(option);
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
84
src/components/custom-echart-maps/index.vue
Normal file
84
src/components/custom-echart-maps/index.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '../../hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartMaps',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
geo: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize, registerMap } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
// series: [],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
// option.series = props.chartData;
|
||||
setOptions(option);
|
||||
registerMap(props.name, props.geo);
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
91
src/components/custom-echart-mixin/index.vue
Normal file
91
src/components/custom-echart-mixin/index.vue
Normal file
@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '../../hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartMixin',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
label: {
|
||||
show: true,
|
||||
backgroundColor: '#333',
|
||||
},
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'bar',
|
||||
type: 'bar',
|
||||
data: [],
|
||||
itemStyle: {
|
||||
barWidth: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
let typeArr = Array.from(new Set(props.chartData.map((item) => item.type)));
|
||||
let xAxisData = Array.from(new Set(props.chartData.map((item) => item.name)));
|
||||
let seriesData = [];
|
||||
typeArr.forEach((type, index) => {
|
||||
const barStyle = props.option?.barStyle ?? {};
|
||||
let obj = { name: type, ...barStyle };
|
||||
let chartArr = props.chartData.filter((item) => type === item.type);
|
||||
obj['data'] = chartArr.map((item) => item.value);
|
||||
obj['type'] = chartArr[0].seriesType;
|
||||
obj['stack'] = chartArr[0].stack;
|
||||
obj['itemStyle'] = chartArr[0].itemStyle;
|
||||
seriesData.push(obj);
|
||||
});
|
||||
option.series = seriesData;
|
||||
option.xAxis.data = xAxisData;
|
||||
setOptions(option);
|
||||
}
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
119
src/components/custom-echart-pictorial-bar/index.vue
Normal file
119
src/components/custom-echart-pictorial-bar/index.vue
Normal file
@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '../../hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'customEchartPictorialBar',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '6%',
|
||||
top: '11%',
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
formatter: '{b}',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pictorialBar',
|
||||
barCategoryGap: '40%',
|
||||
barWidth: '100%',
|
||||
symbol: 'path://M0,10 L10,10 C5.5,10 5.5,5 5,0 C4.5,5 4.5,10 0,10 z',
|
||||
data: [],
|
||||
labelLine: { show: true },
|
||||
z: 10,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: '#000001', // 起始颜色
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#0175b6', // 结束颜色
|
||||
},
|
||||
],
|
||||
global: false, // 默认为 false
|
||||
},
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'top',
|
||||
formatter: '{c}',
|
||||
color: 'white',
|
||||
fontSize: 14,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
setOptions(option);
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
73
src/components/custom-echart-pie-3d/index.vue
Normal file
73
src/components/custom-echart-pie-3d/index.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '../../hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartPie3d',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
series: [],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.series = props.chartData;
|
||||
setOptions(option);
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
73
src/components/custom-echart-pie-gauge/index.vue
Normal file
73
src/components/custom-echart-pie-gauge/index.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '../../hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartPieGauge',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
series: [],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.series = props.chartData;
|
||||
setOptions(option);
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
89
src/components/custom-echart-pie/index.vue
Normal file
89
src/components/custom-echart-pie/index.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '../../hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartPie',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
formatter: '{b} ({c})',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: '72%',
|
||||
center: ['50%', '55%'],
|
||||
data: [],
|
||||
labelLine: { show: true },
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b} \n ({d}%)',
|
||||
color: '#B1B9D3',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.series[0].data = props.chartData;
|
||||
setOptions(option);
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
92
src/components/custom-echart-radar/index.vue
Normal file
92
src/components/custom-echart-radar/index.vue
Normal file
@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '../../hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartRadar',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'radar',
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
title: {
|
||||
text: '',
|
||||
},
|
||||
legend: {},
|
||||
radar: {},
|
||||
tooltip: {},
|
||||
series: [
|
||||
{
|
||||
type: 'radar',
|
||||
data: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
let typeArr = Array.from(new Set(props.chartData.map((item) => item.type)));
|
||||
let indicator = Array.from(
|
||||
new Set(
|
||||
props.chartData.map((item) => {
|
||||
let { name, max } = item;
|
||||
return { name, max };
|
||||
})
|
||||
)
|
||||
);
|
||||
let data = [];
|
||||
typeArr.forEach((type) => {
|
||||
const radarStyle = props.option?.radarStyle ?? {};
|
||||
let obj = { name: type, ...radarStyle };
|
||||
let chartArr = props.chartData.filter((item) => type === item.type);
|
||||
obj['value'] = chartArr.map((item) => item.value);
|
||||
data.push(obj);
|
||||
});
|
||||
option.radar.indicator = indicator;
|
||||
option.series[0]['data'] = data;
|
||||
setOptions(option);
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
73
src/components/custom-echart-scatter-blister/index.vue
Normal file
73
src/components/custom-echart-scatter-blister/index.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '../../hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'customEchartScatterBlister',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
series: [],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.series = props.chartData;
|
||||
setOptions(option);
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
88
src/components/custom-echart-water-droplet/index.vue
Normal file
88
src/components/custom-echart-water-droplet/index.vue
Normal file
@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '../../hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'customEchartWaterDroplet',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
formatter: '{b} ({c})',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: '72%',
|
||||
center: ['50%', '55%'],
|
||||
data: [],
|
||||
labelLine: { show: true },
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b} \n ({d}%)',
|
||||
color: '#B1B9D3',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
setOptions(props.option);
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
73
src/components/custom-echart-word-cloud/index.vue
Normal file
73
src/components/custom-echart-word-cloud/index.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '../../hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartWordCloud',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
series: [],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.series = props.chartData;
|
||||
setOptions(option);
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
39
src/components/custom-iframe/index.vue
Normal file
39
src/components/custom-iframe/index.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<iframe v-if="state.url" :src="state.url" frameborder="0" width="100%" height="100%" @load="onLoad"></iframe>
|
||||
</template>
|
||||
|
||||
<script setup name="custom-iframe">
|
||||
import { reactive, watch } from 'vue';
|
||||
import { isExternal } from '@/utils/validate';
|
||||
|
||||
const props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['on-load']);
|
||||
|
||||
const state = reactive({
|
||||
url: '',
|
||||
loaded: false,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.url,
|
||||
(val) => {
|
||||
if (isExternal(val)) {
|
||||
state.url = val;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const onLoad = () => {
|
||||
state.loaded = true;
|
||||
emit('on-load', state.loaded);
|
||||
};
|
||||
</script>
|
105
src/components/custom-import-excel/index.vue
Normal file
105
src/components/custom-import-excel/index.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="state.visible"
|
||||
draggable
|
||||
append-to-body
|
||||
:title="title"
|
||||
width="50%"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
@close="onClose"
|
||||
>
|
||||
<div class="import-tips">
|
||||
<p>{{ tips }}</p>
|
||||
<el-button v-if="templateUrl" type="primary" icon="download" text @click="emit('on-download', templateUrl)">下载模板</el-button>
|
||||
</div>
|
||||
<el-upload ref="uploadRef" drag action="#" :show-file-list="true" accept=".xlsx,.xls" :limit="1" :http-request="onUploadExcel">
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将文件放在此处或单击上传</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">仅允许导入xls、xlsx格式文件,excel文件大小小于500kb</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="onConfirm"> 确定导入</el-button>
|
||||
<el-button @click="onClose"> 取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup name="custom-import-excel">
|
||||
import { reactive, ref, shallowRef } from 'vue';
|
||||
import { isEmpty } from '@/utils';
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '文件导入',
|
||||
},
|
||||
tips: {
|
||||
type: String,
|
||||
default: '提示:导入前请先下载模板填写信息,然后再导入!',
|
||||
},
|
||||
templateUrl: {
|
||||
type: [String, URL],
|
||||
default: '',
|
||||
},
|
||||
// options: {
|
||||
// type: Object,
|
||||
// default: () => {
|
||||
// return {
|
||||
// tips: '提示:导入前请先下载模板填写信息,然后再导入!',
|
||||
// };
|
||||
// },
|
||||
// },
|
||||
});
|
||||
const emit = defineEmits(['on-confirm', 'on-close', 'on-download']);
|
||||
|
||||
const uploadRef = ref(null);
|
||||
const formDate = shallowRef(null);
|
||||
const state = reactive({
|
||||
visible: false,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const onUploadExcel = ({ file }) => {
|
||||
if (isEmpty(file.name)) return;
|
||||
formDate.value = new FormData();
|
||||
formDate.value.append('file', file);
|
||||
};
|
||||
|
||||
const onConfirm = () => {
|
||||
emit('on-confirm', formDate.value);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
uploadRef?.value && uploadRef.value.clearFiles();
|
||||
state.visible = false;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
show: () => {
|
||||
formDate.value = null;
|
||||
state.visible = true;
|
||||
},
|
||||
hide: () => {
|
||||
onClose();
|
||||
},
|
||||
clear: () => {
|
||||
uploadRef?.value && uploadRef.value.clearFiles();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.import {
|
||||
&-tips {
|
||||
@include flex-row();
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: #979797;
|
||||
|
||||
p {
|
||||
flex: 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
244
src/components/custom-rank-list/index.vue
Normal file
244
src/components/custom-rank-list/index.vue
Normal file
@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<div class="go-tables-rank" :style="`color: ${textColor}`">
|
||||
<div v-for="(item, i) in status.rows" :key="item.toString() + item.scroll" class="row-item" :style="`height: ${status.heights[i]}px;`">
|
||||
<div class="ranking-info">
|
||||
<div class="rank" :style="`color: ${color};font-size: ${indexFontSize}px`">No.{{ item.ranking }}</div>
|
||||
<div class="info-name" :style="`font-size: ${leftFontSize}px`" v-html="item.name" />
|
||||
<div class="ranking-value" :style="`color: ${textColor};font-size: ${rightFontSize}px`">
|
||||
{{ status.mergedConfig.valueFormatter ? status.mergedConfig.valueFormatter(item) : item.value }}
|
||||
{{ unit }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ranking-column" :style="`border-color: ${borderColor}`">
|
||||
<div class="inside-column" :style="`width: ${item.percent}%;background-color: ${color}`">
|
||||
<div class="shine" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="custom-rank-list">
|
||||
import { onUnmounted, reactive, toRefs, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
chartConfig: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const { w, h } = toRefs(props.chartConfig.attr);
|
||||
const { rowNum, unit, color, textColor, borderColor, indexFontSize, leftFontSize, rightFontSize } = toRefs(props.chartConfig.option);
|
||||
|
||||
const status = reactive({
|
||||
mergedConfig: props.chartConfig.option,
|
||||
rowsData: [],
|
||||
rows: [
|
||||
{
|
||||
scroll: 0,
|
||||
ranking: 1,
|
||||
name: '',
|
||||
value: '',
|
||||
percent: 0,
|
||||
},
|
||||
],
|
||||
heights: [0],
|
||||
animationIndex: 0,
|
||||
animationHandler: 0,
|
||||
updater: 0,
|
||||
avgHeight: 0,
|
||||
});
|
||||
|
||||
const calcRowsData = () => {
|
||||
let { dataset, rowNum, sort } = status.mergedConfig;
|
||||
// @ts-ignore
|
||||
sort &&
|
||||
dataset.sort(({ value: a }, { value: b }) => {
|
||||
if (a > b) return -1;
|
||||
if (a < b) return 1;
|
||||
if (a === b) return 0;
|
||||
});
|
||||
// @ts-ignore
|
||||
const value = dataset.map(({ value }) => value);
|
||||
const min = Math.min(...value) || 0;
|
||||
// abs of min
|
||||
const minAbs = Math.abs(min);
|
||||
const max = Math.max(...value) || 0;
|
||||
// abs of max
|
||||
const maxAbs = Math.abs(max);
|
||||
const total = max + minAbs;
|
||||
dataset = dataset.map((row, i) => ({
|
||||
...row,
|
||||
ranking: i + 1,
|
||||
percent: ((row.value + minAbs) / total) * 100,
|
||||
}));
|
||||
const rowLength = dataset.length;
|
||||
if (rowLength > rowNum && rowLength < 2 * rowNum) {
|
||||
dataset = [...dataset, ...dataset];
|
||||
}
|
||||
dataset = dataset.map((d, i) => ({ ...d, scroll: i }));
|
||||
status.rowsData = dataset;
|
||||
status.rows = dataset;
|
||||
};
|
||||
|
||||
const calcHeights = (onresize = false) => {
|
||||
const { rowNum, dataset } = status.mergedConfig;
|
||||
const avgHeight = h.value / rowNum;
|
||||
status.avgHeight = avgHeight;
|
||||
|
||||
if (!onresize) status.heights = new Array(dataset.length).fill(avgHeight);
|
||||
};
|
||||
|
||||
const animation = async (start = false) => {
|
||||
let { avgHeight, animationIndex, mergedConfig, rowsData, updater } = status;
|
||||
const { waitTime, carousel, rowNum } = mergedConfig;
|
||||
const rowLength = rowsData.length;
|
||||
if (rowNum >= rowLength) return;
|
||||
if (start) {
|
||||
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
||||
if (updater !== status.updater) return;
|
||||
}
|
||||
const animationNum = carousel === 'single' ? 1 : rowNum;
|
||||
let rows = rowsData.slice(animationIndex);
|
||||
rows.push(...rowsData.slice(0, animationIndex));
|
||||
status.rows = rows.slice(0, rowNum + 1);
|
||||
status.heights = new Array(rowLength).fill(avgHeight);
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
if (updater !== status.updater) return;
|
||||
status.heights.splice(0, animationNum, ...new Array(animationNum).fill(0));
|
||||
animationIndex += animationNum;
|
||||
const back = animationIndex - rowLength;
|
||||
if (back >= 0) animationIndex = back;
|
||||
|
||||
status.animationIndex = animationIndex;
|
||||
status.animationHandler = setTimeout(animation, waitTime * 1000 - 300);
|
||||
};
|
||||
|
||||
const stopAnimation = () => {
|
||||
status.updater = (status.updater + 1) % 999999;
|
||||
if (!status.animationHandler) return;
|
||||
clearTimeout(status.animationHandler);
|
||||
};
|
||||
|
||||
const onRestart = async () => {
|
||||
try {
|
||||
if (!status.mergedConfig) return;
|
||||
let { dataset, rowNum, sort } = status.mergedConfig;
|
||||
stopAnimation();
|
||||
calcRowsData();
|
||||
let flag = true;
|
||||
if (dataset.length <= rowNum) {
|
||||
flag = false;
|
||||
}
|
||||
calcHeights(flag);
|
||||
animation(flag);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
onRestart();
|
||||
|
||||
watch(
|
||||
() => w.value,
|
||||
() => {
|
||||
onRestart();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => h.value,
|
||||
() => {
|
||||
onRestart();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => rowNum.value,
|
||||
() => {
|
||||
onRestart();
|
||||
}
|
||||
);
|
||||
|
||||
// 数据更新(配置时触发)
|
||||
watch(
|
||||
() => props.chartConfig.option.dataset,
|
||||
() => {
|
||||
onRestart();
|
||||
},
|
||||
{
|
||||
deep: false,
|
||||
}
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAnimation();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.go-tables-rank {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.row-item {
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ranking-info {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
align-items: center;
|
||||
|
||||
.rank {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.info-name {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ranking-column {
|
||||
border-bottom: 2px solid #1370fb80;
|
||||
margin-top: 5px;
|
||||
|
||||
.inside-column {
|
||||
position: relative;
|
||||
height: 6px;
|
||||
margin-bottom: 2px;
|
||||
border-radius: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shine {
|
||||
position: absolute;
|
||||
left: 0%;
|
||||
top: 2px;
|
||||
height: 2px;
|
||||
width: 50px;
|
||||
transform: translateX(-100%);
|
||||
background: radial-gradient(rgb(40, 248, 255) 5%, transparent 80%);
|
||||
animation: shine 3s ease-in-out infinite alternate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
80% {
|
||||
left: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
left: 100%;
|
||||
transform: translateX(0%);
|
||||
}
|
||||
}
|
||||
</style>
|
173
src/components/custom-rich-editor/index.vue
Normal file
173
src/components/custom-rich-editor/index.vue
Normal file
@ -0,0 +1,173 @@
|
||||
<!--
|
||||
* @Description:
|
||||
* @Author: zenghua.wang
|
||||
* @Date: 2024-03-24 11:04:52
|
||||
* @LastEditors: zenghua.wang “1048523306@qq.com”
|
||||
* @LastEditTime: 2025-01-20 09:34:23
|
||||
-->
|
||||
<template>
|
||||
<section class="rich-editor">
|
||||
<Toolbar v-show="toolbarShow" class="rich-editor-toolbar" :editor="refEditor" :default-config="options.toolbarConfig" :mode="mode" />
|
||||
<Editor
|
||||
v-model="valueHtml"
|
||||
class="rich-editor-toolbar"
|
||||
:style="styleEditor"
|
||||
:default-config="options.editorConfig"
|
||||
:mode="mode"
|
||||
@on-created="handleCreated"
|
||||
@on-change="handleChange"
|
||||
@on-destroyed="handleDestroyed"
|
||||
@on-focus="handleFocus"
|
||||
@on-blur="handleBlur"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@wangeditor/editor/dist/css/style.css'; // 引入 css
|
||||
import { shallowRef, ref, computed, nextTick, onBeforeUnmount, onMounted } from 'vue';
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
|
||||
import { isEmpty } from '@/utils';
|
||||
// import { CommonUpload, UploadImageFromEditor } from '@/apis/common';
|
||||
import { imageUpload } from './upFile';
|
||||
|
||||
const { VITE_APP_OSS_URL } = import.meta.env;
|
||||
|
||||
export default {
|
||||
name: 'CustomRichEditor',
|
||||
components: { Editor, Toolbar },
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'default', //'default' 或 'simple'
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
toolbarConfig: {},
|
||||
editorConfig: {
|
||||
placeholder: '请输入内容...',
|
||||
readOnly: false,
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
server: '',
|
||||
base64LimitSize: 10 * 1024, //10kb
|
||||
maxFileSize: 10 * 1024 * 1024, //10M
|
||||
maxNumberOfFiles: 10,
|
||||
allowedFileTypes: ['image/*'],
|
||||
customUpload: imageUpload,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
toolbarShow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['focus', 'blur', 'change', 'update:value'],
|
||||
setup(props, cxt) {
|
||||
const refEditor = shallowRef();
|
||||
const valueHtml = ref('');
|
||||
|
||||
const styleEditor = computed(() => {
|
||||
return {
|
||||
height: props.options.contentHeight || '300px',
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建
|
||||
* @param {*} editor
|
||||
*/
|
||||
const handleCreated = (editor) => {
|
||||
refEditor.value = editor;
|
||||
props.readOnly ? editor.disable() : editor.enable();
|
||||
};
|
||||
/**
|
||||
* 组件内容变化
|
||||
* @param {*} editor
|
||||
*/
|
||||
const handleChange = (editor) => {
|
||||
cxt.emit('change', editor);
|
||||
cxt.emit('update:value', valueHtml.value);
|
||||
};
|
||||
/**
|
||||
* 组件销毁
|
||||
* @param {*} editor
|
||||
*/
|
||||
const handleDestroyed = (editor) => {
|
||||
valueHtml.value = '';
|
||||
};
|
||||
/**
|
||||
* 光标处于编辑区
|
||||
* @param {*} editor
|
||||
*/
|
||||
const handleFocus = (editor) => {
|
||||
cxt.emit('focus', editor);
|
||||
};
|
||||
/**
|
||||
* 光标离开编辑区
|
||||
* @param {*} editor
|
||||
*/
|
||||
const handleBlur = (editor) => {
|
||||
cxt.emit('blur', editor);
|
||||
};
|
||||
/**
|
||||
* 挂载
|
||||
*/
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (props?.value) {
|
||||
valueHtml.value = props.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
/**
|
||||
* 组件销毁时,也及时销毁编辑器
|
||||
*/
|
||||
onBeforeUnmount(() => {
|
||||
if (!refEditor?.value) return;
|
||||
refEditor.value.destroy();
|
||||
});
|
||||
|
||||
return {
|
||||
refEditor,
|
||||
valueHtml,
|
||||
styleEditor,
|
||||
handleCreated,
|
||||
handleChange,
|
||||
handleDestroyed,
|
||||
handleFocus,
|
||||
handleBlur,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.rich-editor {
|
||||
border: 1px solid $color-border;
|
||||
z-index: 9999;
|
||||
&-toolbar {
|
||||
border-bottom: 1px solid $color-border;
|
||||
:deep(.w-e-bar-divider) {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
&-editor {
|
||||
overflow-y: hidden;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
10
src/components/custom-rich-editor/upFile.js
Normal file
10
src/components/custom-rich-editor/upFile.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { CommonUpload } from '../../apis/common';
|
||||
|
||||
export async function imageUpload(file, insertFn) {
|
||||
let formData = new FormData();
|
||||
formData.append('file', file);
|
||||
let res = await CommonUpload(formData);
|
||||
if (res.code == 200) {
|
||||
insertFn(res.data.url, file.name, res.data.key);
|
||||
}
|
||||
}
|
366
src/components/custom-scroll-board/index.vue
Normal file
366
src/components/custom-scroll-board/index.vue
Normal file
@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div class="dv-scroll-board">
|
||||
<div v-if="status.header.length && status.mergedConfig" class="header" :style="`background-color: ${status.mergedConfig.headerBGC};`">
|
||||
<div
|
||||
v-for="(headerItem, i) in status.header"
|
||||
:key="`${headerItem}${i}`"
|
||||
class="header-item"
|
||||
:style="`
|
||||
height: ${status.mergedConfig.headerHeight}px;
|
||||
line-height: ${status.mergedConfig.headerHeight}px;
|
||||
width: ${status.widths[i]}px;
|
||||
`"
|
||||
:align="status.aligns[i]"
|
||||
v-html="headerItem"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div v-if="status.mergedConfig" class="rows" :style="`height: ${h - (status.header.length ? status.mergedConfig.headerHeight : 0)}px;`">
|
||||
<div
|
||||
v-for="(row, ri) in status.rows"
|
||||
:key="`${row.toString()}${row.scroll}`"
|
||||
class="row-item"
|
||||
:style="`
|
||||
height: ${status.heights[ri]}px;
|
||||
line-height: ${status.heights[ri]}px;
|
||||
background-color: ${status.mergedConfig[row.rowIndex % 2 === 0 ? 'evenRowBGC' : 'oddRowBGC']};
|
||||
`"
|
||||
>
|
||||
<div
|
||||
v-for="(ceil, ci) in row.ceils"
|
||||
:key="`${ceil}${ri}${ci}`"
|
||||
class="ceil"
|
||||
:style="`width: ${status.widths[ci]}px;`"
|
||||
:align="status.aligns[ci]"
|
||||
v-html="ceil"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="custom-scroll-board">
|
||||
import { onUnmounted, reactive, toRefs, watch, onMounted } from 'vue';
|
||||
import merge from 'lodash/merge';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
|
||||
const props = defineProps({
|
||||
chartConfig: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { w, h } = toRefs(props.chartConfig.attr);
|
||||
|
||||
const status = reactive({
|
||||
defaultConfig: {
|
||||
/**
|
||||
* @description Board header
|
||||
* @type {Array<String>}
|
||||
* @default header = []
|
||||
* @example header = ['column1', 'column2', 'column3']
|
||||
*/
|
||||
header: [],
|
||||
/**
|
||||
* @description Board dataset
|
||||
* @type {Array<Array>}
|
||||
* @default dataset = []
|
||||
*/
|
||||
dataset: [],
|
||||
/**
|
||||
* @description Row num
|
||||
* @type {Number}
|
||||
* @default rowNum = 5
|
||||
*/
|
||||
rowNum: 5,
|
||||
/**
|
||||
* @description Header background color
|
||||
* @type {String}
|
||||
* @default headerBGC = '#00BAFF'
|
||||
*/
|
||||
headerBGC: '#00BAFF',
|
||||
/**
|
||||
* @description Odd row background color
|
||||
* @type {String}
|
||||
* @default oddRowBGC = '#003B51'
|
||||
*/
|
||||
oddRowBGC: '#003B51',
|
||||
/**
|
||||
* @description Even row background color
|
||||
* @type {String}
|
||||
* @default evenRowBGC = '#003B51'
|
||||
*/
|
||||
evenRowBGC: '#0A2732',
|
||||
/**
|
||||
* @description Scroll wait time
|
||||
* @type {Number}
|
||||
* @default waitTime = 2
|
||||
*/
|
||||
waitTime: 2,
|
||||
/**
|
||||
* @description Header height
|
||||
* @type {Number}
|
||||
* @default headerHeight = 35
|
||||
*/
|
||||
headerHeight: 35,
|
||||
/**
|
||||
* @description Column width
|
||||
* @type {Array<Number>}
|
||||
* @default columnWidth = []
|
||||
*/
|
||||
columnWidth: [],
|
||||
/**
|
||||
* @description Column align
|
||||
* @type {Array<String>}
|
||||
* @default align = []
|
||||
* @example align = ['left', 'center', 'right']
|
||||
*/
|
||||
align: [],
|
||||
/**
|
||||
* @description Show index
|
||||
* @type {Boolean}
|
||||
* @default index = false
|
||||
*/
|
||||
index: false,
|
||||
/**
|
||||
* @description index Header
|
||||
* @type {String}
|
||||
* @default indexHeader = '#'
|
||||
*/
|
||||
indexHeader: '#',
|
||||
/**
|
||||
* @description Carousel type
|
||||
* @type {String}
|
||||
* @default carousel = 'single'
|
||||
* @example carousel = 'single' | 'page'
|
||||
*/
|
||||
carousel: 'single',
|
||||
/**
|
||||
* @description Pause scroll when mouse hovered
|
||||
* @type {Boolean}
|
||||
* @default hoverPause = true
|
||||
* @example hoverPause = true | false
|
||||
*/
|
||||
hoverPause: true,
|
||||
},
|
||||
mergedConfig: props.chartConfig.option,
|
||||
header: [],
|
||||
rowsData: [],
|
||||
rows: [
|
||||
{
|
||||
ceils: [],
|
||||
rowIndex: 0,
|
||||
scroll: 0,
|
||||
},
|
||||
],
|
||||
widths: [],
|
||||
heights: [0],
|
||||
avgHeight: 0,
|
||||
aligns: [],
|
||||
animationIndex: 0,
|
||||
animationHandler: 0,
|
||||
updater: 0,
|
||||
needCalc: false,
|
||||
});
|
||||
|
||||
const calcData = () => {
|
||||
mergeConfig();
|
||||
calcHeaderData();
|
||||
calcRowsData();
|
||||
calcWidths();
|
||||
calcHeights();
|
||||
calcAligns();
|
||||
animation(true);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
calcData();
|
||||
});
|
||||
|
||||
const mergeConfig = () => {
|
||||
status.mergedConfig = merge(cloneDeep(status.defaultConfig), props.chartConfig.option);
|
||||
};
|
||||
|
||||
const calcHeaderData = () => {
|
||||
let { header, index, indexHeader } = status.mergedConfig;
|
||||
if (!header.length) {
|
||||
status.header = [];
|
||||
return;
|
||||
}
|
||||
header = [...header];
|
||||
if (index) header.unshift(indexHeader);
|
||||
status.header = header;
|
||||
};
|
||||
|
||||
const calcRowsData = () => {
|
||||
let { dataset, index, headerBGC, rowNum } = status.mergedConfig;
|
||||
if (index) {
|
||||
dataset = dataset.map((row, i) => {
|
||||
row = [...row];
|
||||
const indexTag = `<span class="index" style="background-color: ${headerBGC};border-radius: 3px;padding: 0px 3px;">${i + 1}</span>`;
|
||||
row.unshift(indexTag);
|
||||
return row;
|
||||
});
|
||||
}
|
||||
dataset = dataset.map((ceils, i) => ({ ceils, rowIndex: i }));
|
||||
const rowLength = dataset.length;
|
||||
if (rowLength > rowNum && rowLength < 2 * rowNum) {
|
||||
dataset = [...dataset, ...dataset];
|
||||
}
|
||||
dataset = dataset.map((d, i) => ({ ...d, scroll: i }));
|
||||
|
||||
status.rowsData = dataset;
|
||||
status.rows = dataset;
|
||||
};
|
||||
|
||||
const calcWidths = () => {
|
||||
const { mergedConfig, rowsData } = status;
|
||||
const { columnWidth, header } = mergedConfig;
|
||||
const usedWidth = columnWidth.reduce((all, ws) => all + ws, 0);
|
||||
let columnNum = 0;
|
||||
if (rowsData[0]) {
|
||||
columnNum = rowsData[0].ceils.length;
|
||||
} else if (header.length) {
|
||||
columnNum = header.length;
|
||||
}
|
||||
const avgWidth = (w.value - usedWidth) / (columnNum - columnWidth.length);
|
||||
const widths = new Array(columnNum).fill(avgWidth);
|
||||
status.widths = merge(widths, columnWidth);
|
||||
};
|
||||
|
||||
const calcHeights = (onresize = false) => {
|
||||
const { mergedConfig, header } = status;
|
||||
const { headerHeight, rowNum, dataset } = mergedConfig;
|
||||
let allHeight = h.value;
|
||||
if (header.length) allHeight -= headerHeight;
|
||||
const avgHeight = allHeight / rowNum;
|
||||
status.avgHeight = avgHeight;
|
||||
if (!onresize) status.heights = new Array(dataset.length).fill(avgHeight);
|
||||
};
|
||||
|
||||
const calcAligns = () => {
|
||||
const { header, mergedConfig } = status;
|
||||
|
||||
const columnNum = header.length;
|
||||
|
||||
let aligns = new Array(columnNum).fill('left');
|
||||
|
||||
const { align } = mergedConfig;
|
||||
|
||||
status.aligns = merge(aligns, align);
|
||||
};
|
||||
|
||||
const animation = async (start = false) => {
|
||||
const { needCalc } = status;
|
||||
|
||||
if (needCalc) {
|
||||
calcRowsData();
|
||||
calcHeights();
|
||||
status.needCalc = false;
|
||||
}
|
||||
let { avgHeight, animationIndex, mergedConfig, rowsData, updater } = status;
|
||||
const { waitTime, carousel, rowNum } = mergedConfig;
|
||||
|
||||
const rowLength = rowsData.length;
|
||||
if (rowNum >= rowLength) return;
|
||||
if (start) {
|
||||
await new Promise((resolve) => setTimeout(resolve, waitTime * 1000));
|
||||
if (updater !== status.updater) return;
|
||||
}
|
||||
const animationNum = carousel === 'single' ? 1 : rowNum;
|
||||
let rows = rowsData.slice(animationIndex);
|
||||
rows.push(...rowsData.slice(0, animationIndex));
|
||||
status.rows = rows.slice(0, carousel === 'page' ? rowNum * 2 : rowNum + 1);
|
||||
status.heights = new Array(rowLength).fill(avgHeight);
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
if (updater !== status.updater) return;
|
||||
status.heights.splice(0, animationNum, ...new Array(animationNum).fill(0));
|
||||
animationIndex += animationNum;
|
||||
const back = animationIndex - rowLength;
|
||||
if (back >= 0) animationIndex = back;
|
||||
status.animationIndex = animationIndex;
|
||||
status.animationHandler = setTimeout(animation, waitTime * 1000 - 300);
|
||||
};
|
||||
|
||||
const stopAnimation = () => {
|
||||
status.updater = (status.updater + 1) % 999999;
|
||||
if (!status.animationHandler) return;
|
||||
clearTimeout(status.animationHandler);
|
||||
};
|
||||
|
||||
const onRestart = async () => {
|
||||
try {
|
||||
if (!status.mergedConfig) return;
|
||||
stopAnimation();
|
||||
calcData();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => w.value,
|
||||
() => {
|
||||
onRestart();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => h.value,
|
||||
() => {
|
||||
onRestart();
|
||||
}
|
||||
);
|
||||
|
||||
// 数据更新
|
||||
watch(
|
||||
() => props.chartConfig.option,
|
||||
() => {
|
||||
onRestart();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAnimation();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dv-scroll-board {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
|
||||
.text {
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 15px;
|
||||
|
||||
.header-item {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.rows {
|
||||
overflow: hidden;
|
||||
|
||||
.row-item {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
59
src/components/custom-table-operate/index.vue
Normal file
59
src/components/custom-table-operate/index.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<el-dropdown popper-class="custom-table-operate">
|
||||
<el-icon class="custom-table-operate__more">
|
||||
<template v-if="show">
|
||||
<More />
|
||||
</template>
|
||||
</el-icon>
|
||||
<template v-if="show" #dropdown>
|
||||
<el-dropdown-menu v-if="!isEmpty(actions)">
|
||||
<template v-for="item in actions" :key="item.name">
|
||||
<el-dropdown-item v-if="onPermission(item)" @click="item.event(data)">
|
||||
<el-button :type="item.type ?? 'primary'" :icon="formatterIcon(item)" :size="item.size" :disabled="item.disabled" text>
|
||||
{{ formatterName(item) }}
|
||||
</el-button>
|
||||
</el-dropdown-item>
|
||||
</template>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
<script setup name="custom-table-operate">
|
||||
import { isEmpty } from '@/utils';
|
||||
|
||||
const props = defineProps({
|
||||
actions: { type: Array, default: () => [] },
|
||||
data: { type: Object, default: () => {} },
|
||||
show: { type: Boolean, default: true },
|
||||
});
|
||||
|
||||
const onPermission = (row) => {
|
||||
if (row.auth === undefined) return true;
|
||||
return typeof row.auth === 'function' ? row.auth(props.data) : row.auth;
|
||||
};
|
||||
|
||||
const formatterName = (row) => {
|
||||
return typeof row.name === 'function' ? row.name(props.data) : row.name;
|
||||
};
|
||||
|
||||
const formatterIcon = (row) => {
|
||||
return typeof row.icon === 'function' ? row.icon(props.data) : row.icon;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.custom-table-operate {
|
||||
&__more {
|
||||
padding: 20px 5px;
|
||||
font-size: 20px;
|
||||
color: var(--el-color-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
.el-button {
|
||||
&,
|
||||
&:hover &.is-text:hover,
|
||||
&.is-text:not(.is-disabled):hover {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
120
src/components/custom-table-tree/index.vue
Normal file
120
src/components/custom-table-tree/index.vue
Normal file
@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div :class="`custom-table-tree ${shadow ? 'custom-table-tree__shadow' : ''}`">
|
||||
<div v-if="title" class="title">{{ title }}</div>
|
||||
<div class="panel">
|
||||
<el-input v-if="filter" v-model="state.keyword" clearable placeholder="请输入关键字筛选" class="panel-filter" />
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="state.list"
|
||||
:node-key="option.nodeKey"
|
||||
:show-checkbox="option.showCheckbox"
|
||||
:default-expanded-keys="option.defaultExpandedKeys"
|
||||
:default-checked-keys="option.defaultCheckedKeys"
|
||||
:default-expand-all="option.defaultExpandAll"
|
||||
:props="option.props ?? option.defaultProps"
|
||||
:filter-node-method="filterNodeMethod"
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #default="{ data: rows }">
|
||||
<slot :data="rows"></slot>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup name="custom-table-tree">
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: '' },
|
||||
shadow: { type: Boolean, default: false },
|
||||
filter: { type: Boolean, default: false },
|
||||
data: { type: Array, default: () => [] },
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
nodeKey: 'id',
|
||||
showCheckbox: false,
|
||||
props: {},
|
||||
defaultProps: {
|
||||
children: 'children',
|
||||
label: 'label',
|
||||
},
|
||||
defaultExpandedKeys: [],
|
||||
defaultCheckedKeys: [],
|
||||
defaultExpandAll: false,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['node-click']);
|
||||
|
||||
const treeRef = ref(null);
|
||||
const state = reactive({
|
||||
keyword: '',
|
||||
list: [],
|
||||
});
|
||||
const label = props.option.props?.label ?? 'label';
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(val) => {
|
||||
state.list = val;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => state.keyword,
|
||||
(val) => {
|
||||
treeRef.value.filter(val);
|
||||
}
|
||||
);
|
||||
|
||||
const filterNodeMethod = (value, data) => {
|
||||
if (!value) return true;
|
||||
return data[label].includes(value);
|
||||
};
|
||||
|
||||
const handleNodeClick = (data, node) => {
|
||||
emit('node-click', data, node);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.custom-table-tree {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
min-width: 200px;
|
||||
@include flex-column();
|
||||
|
||||
&__shadow {
|
||||
box-shadow: 2px 0px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.title {
|
||||
height: 36px;
|
||||
padding: 0 20px;
|
||||
line-height: 36px;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 16px 10px 10px;
|
||||
|
||||
&-filter {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tree {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
45
src/components/index.js
Normal file
45
src/components/index.js
Normal file
@ -0,0 +1,45 @@
|
||||
import SvgIcon from './svg-icon';
|
||||
import CustomIframe from './custom-iframe';
|
||||
import CustomTableOperate from './custom-table-operate';
|
||||
import CustomTableTree from './custom-table-tree';
|
||||
import CustomCarouselPicture from './custom-carousel-picture';
|
||||
import CustomImportExcel from './custom-import-excel';
|
||||
import CustomRichEditor from './custom-rich-editor';
|
||||
import CustomEchartBar from './custom-echart-bar';
|
||||
import CustomEchartPie from './custom-echart-pie';
|
||||
import CustomEchartLine from './custom-echart-line';
|
||||
import CustomEchartRadar from './custom-echart-radar';
|
||||
import CustomEchartMixin from './custom-echart-mixin';
|
||||
import customEchartPictorialBar from './custom-echart-pictorial-bar';
|
||||
import CustomEchartLineLine from './custom-echart-line-line';
|
||||
import CustomEchartBubble from './custom-echart-bubble';
|
||||
import CustomEchartPie3d from './custom-echart-pie-3d';
|
||||
import CustomEchartWaterDroplet from './custom-echart-water-droplet';
|
||||
import CustomEchartPieGauge from './custom-echart-pie-gauge';
|
||||
import CustomEchartWordCloud from './custom-echart-word-cloud';
|
||||
import customEchartScatterBlister from './custom-echart-scatter-blister';
|
||||
import customEchartMaps from './custom-echart-maps';
|
||||
|
||||
export {
|
||||
SvgIcon,
|
||||
CustomIframe,
|
||||
CustomTableOperate,
|
||||
CustomTableTree,
|
||||
CustomCarouselPicture,
|
||||
CustomImportExcel,
|
||||
CustomRichEditor,
|
||||
CustomEchartBar,
|
||||
CustomEchartPie,
|
||||
CustomEchartLine,
|
||||
CustomEchartRadar,
|
||||
CustomEchartMixin,
|
||||
customEchartPictorialBar,
|
||||
CustomEchartLineLine,
|
||||
CustomEchartBubble,
|
||||
CustomEchartPie3d,
|
||||
CustomEchartWaterDroplet,
|
||||
CustomEchartPieGauge,
|
||||
CustomEchartWordCloud,
|
||||
customEchartScatterBlister,
|
||||
customEchartMaps,
|
||||
};
|
81
src/components/svg-icon/index.vue
Normal file
81
src/components/svg-icon/index.vue
Normal file
@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" />
|
||||
<svg v-else :class="svgClass" aria-hidden="true" :style="styleSize()">
|
||||
<use :xlink:href="iconName" />
|
||||
<title v-if="title">{{ title }}</title>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup name="svg-icon">
|
||||
import { computed } from 'vue';
|
||||
import { setPx } from '@/utils';
|
||||
|
||||
const props = defineProps({
|
||||
// svg图片名称
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 16,
|
||||
},
|
||||
// class样式名称,如果svg标签需要添加class样式,那么就需要该属性来添加svg的样式
|
||||
className: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
//是否外部链接
|
||||
const isExternal = computed(() => {
|
||||
return /^(https?:|mailto:|tel:)/.test(props.name);
|
||||
});
|
||||
|
||||
//svg图片名称计算属性
|
||||
const iconName = computed(() => {
|
||||
return `#icon-${props.name}`;
|
||||
});
|
||||
|
||||
//svg样式名称计算属性
|
||||
const svgClass = computed(() => {
|
||||
if (props.className) {
|
||||
return 'svg-icon ' + props.className;
|
||||
} else {
|
||||
return 'svg-icon';
|
||||
}
|
||||
});
|
||||
|
||||
const styleSize = () => {
|
||||
return {
|
||||
fontSize: setPx(props.size),
|
||||
};
|
||||
};
|
||||
|
||||
const styleExternalIcon = computed(() => {
|
||||
return {
|
||||
mask: `url(${props.name}) no-repeat 50% 50%`,
|
||||
'-webkit-mask': `url(${props.name}) no-repeat 50% 50%`,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.svg-external-icon {
|
||||
background-color: currentColor;
|
||||
mask-size: cover !important;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
10
src/config/index.js
Normal file
10
src/config/index.js
Normal file
@ -0,0 +1,10 @@
|
||||
const { VITE_APP_NAME } = import.meta.env;
|
||||
|
||||
export const GenKey = (key, prefix = VITE_APP_NAME) => {
|
||||
return prefix ? `${prefix}_` + key : key;
|
||||
};
|
||||
|
||||
export const CONSTANTS = {
|
||||
PREFIX: `${VITE_APP_NAME}_`,
|
||||
PRIMARY: '#409eff',
|
||||
};
|
32
src/directives/auth.js
Normal file
32
src/directives/auth.js
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @Description: 按钮权限
|
||||
* @Author: zenghua.wang
|
||||
* @Date: 2022-08-30 09:42:47
|
||||
* @LastEditors: zenghua.wang
|
||||
* @LastEditTime: 2024-01-30 13:59:50
|
||||
*/
|
||||
|
||||
// import { CACHE_KEY, useCache } from '@/hooks/web/useCache';
|
||||
|
||||
export function useAuth(app) {
|
||||
app.directive('auth', (el, binding) => {
|
||||
// const { wsCache } = useCache();
|
||||
const { value } = binding;
|
||||
const all_permission = '*:*:*';
|
||||
const permissions = []; //wsCache.get(CACHE_KEY.USER).permissions;
|
||||
|
||||
if (value && value instanceof Array && value.length > 0) {
|
||||
const permissionFlag = value;
|
||||
|
||||
const hasAuth = permissions.some((permission) => {
|
||||
return all_permission === permission || permissionFlag.includes(permission);
|
||||
});
|
||||
|
||||
if (!hasAuth) {
|
||||
el.parentNode && el.parentNode.removeChild(el);
|
||||
}
|
||||
} else {
|
||||
// throw new Error('no auth to access it.');
|
||||
}
|
||||
});
|
||||
}
|
9
src/directives/index.js
Normal file
9
src/directives/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { useAuth } from './auth';
|
||||
|
||||
/**
|
||||
* 指令:v-xxx
|
||||
* @methods auth 按钮权限,用法: v-auth
|
||||
*/
|
||||
export const registerDirective = (app) => {
|
||||
useAuth(app);
|
||||
};
|
5
src/hooks/index.js
Normal file
5
src/hooks/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { getCurrentInstance } from 'vue';
|
||||
|
||||
export const useApp = () => {
|
||||
return getCurrentInstance().appContext?.config?.globalProperties;
|
||||
};
|
84
src/hooks/useBreakpoint.js
Normal file
84
src/hooks/useBreakpoint.js
Normal file
@ -0,0 +1,84 @@
|
||||
import { ref, computed, unref } from 'vue';
|
||||
import { useEventListener } from './useEventListener';
|
||||
|
||||
let globalScreenRef = 0;
|
||||
let globalWidthRef = 0;
|
||||
let globalRealWidthRef = 0;
|
||||
|
||||
const screenMap = new Map();
|
||||
screenMap.set('XS', 480);
|
||||
screenMap.set('SM', 576);
|
||||
screenMap.set('MD', 768);
|
||||
screenMap.set('LG', 992);
|
||||
screenMap.set('XL', 1200);
|
||||
screenMap.set('XXL', 1600);
|
||||
|
||||
export function useBreakpoint() {
|
||||
return {
|
||||
screenRef: computed(() => unref(globalScreenRef)),
|
||||
widthRef: globalWidthRef,
|
||||
realWidthRef: globalRealWidthRef,
|
||||
};
|
||||
}
|
||||
|
||||
// Just call it once
|
||||
export function createBreakpointListen(fn) {
|
||||
const screenRef = ref('XL');
|
||||
const realWidthRef = ref(window.innerWidth);
|
||||
|
||||
function getWindowWidth() {
|
||||
const width = document.body.clientWidth;
|
||||
const xs = screenMap.get('XS');
|
||||
const sm = screenMap.get('SM');
|
||||
const md = screenMap.get('MD');
|
||||
const lg = screenMap.get('LG');
|
||||
const xl = screenMap.get('XL');
|
||||
const xxl = screenMap.set('XXL', 1600);
|
||||
if (width < xs) {
|
||||
screenRef.value = xs;
|
||||
} else if (width < sm) {
|
||||
screenRef.value = sm;
|
||||
} else if (width < md) {
|
||||
screenRef.value = md;
|
||||
} else if (width < lg) {
|
||||
screenRef.value = lg;
|
||||
} else if (width < xl) {
|
||||
screenRef.value = xl;
|
||||
} else {
|
||||
screenRef.value = xxl;
|
||||
}
|
||||
realWidthRef.value = width;
|
||||
}
|
||||
|
||||
useEventListener({
|
||||
el: window,
|
||||
name: 'resize',
|
||||
|
||||
listener: () => {
|
||||
getWindowWidth();
|
||||
resizeFn();
|
||||
},
|
||||
// wait: 100,
|
||||
});
|
||||
|
||||
getWindowWidth();
|
||||
globalScreenRef = computed(() => unref(screenRef));
|
||||
globalWidthRef = computed(() => screenMap.get(unref(screenRef)));
|
||||
globalRealWidthRef = computed(() => unref(realWidthRef));
|
||||
|
||||
function resizeFn() {
|
||||
fn?.({
|
||||
screen: globalScreenRef,
|
||||
width: globalWidthRef,
|
||||
realWidth: globalRealWidthRef,
|
||||
screenMap,
|
||||
});
|
||||
}
|
||||
|
||||
resizeFn();
|
||||
return {
|
||||
screenRef: globalScreenRef,
|
||||
widthRef: globalWidthRef,
|
||||
realWidthRef: globalRealWidthRef,
|
||||
};
|
||||
}
|
121
src/hooks/useEcharts.js
Normal file
121
src/hooks/useEcharts.js
Normal file
@ -0,0 +1,121 @@
|
||||
import { unref, nextTick, watch, computed, ref } from 'vue';
|
||||
import { useDebounceFn, tryOnUnmounted } from '@vueuse/core';
|
||||
import { useTimeoutFn } from './useTimeout';
|
||||
import { useEventListener } from './useEventListener';
|
||||
import { useBreakpoint } from './useBreakpoint';
|
||||
import echarts from '../utils/echarts';
|
||||
|
||||
export const useEcharts = (elRef, theme = 'default') => {
|
||||
const getDarkMode = computed(() => {
|
||||
return theme === 'default' ? 'dark' : theme;
|
||||
});
|
||||
let chartInstance = null;
|
||||
let resizeFn = resize;
|
||||
const cacheOptions = ref({});
|
||||
let removeResizeFn = null;
|
||||
|
||||
resizeFn = useDebounceFn(resize, 200);
|
||||
|
||||
const getOptions = computed(() => {
|
||||
if (getDarkMode.value !== 'dark') {
|
||||
return cacheOptions.value;
|
||||
}
|
||||
return {
|
||||
backgroundColor: 'transparent',
|
||||
...cacheOptions.value,
|
||||
};
|
||||
});
|
||||
|
||||
function initCharts(t = theme) {
|
||||
const el = unref(elRef);
|
||||
if (!el || !unref(el)) {
|
||||
return;
|
||||
}
|
||||
|
||||
chartInstance = echarts.init(el, t);
|
||||
const { removeEvent } = useEventListener({
|
||||
el: window,
|
||||
name: 'resize',
|
||||
listener: resizeFn,
|
||||
});
|
||||
removeResizeFn = removeEvent;
|
||||
const { widthRef } = useBreakpoint();
|
||||
if (unref(widthRef) <= 768 || el.offsetHeight === 0) {
|
||||
useTimeoutFn(() => {
|
||||
resizeFn();
|
||||
}, 30);
|
||||
}
|
||||
}
|
||||
|
||||
function setOptions(options = {}, clear = true) {
|
||||
cacheOptions.value = options;
|
||||
if (unref(elRef)?.offsetHeight === 0) {
|
||||
useTimeoutFn(() => {
|
||||
setOptions(unref(getOptions));
|
||||
}, 30);
|
||||
return;
|
||||
}
|
||||
nextTick(() => {
|
||||
useTimeoutFn(() => {
|
||||
if (!chartInstance) {
|
||||
initCharts(getDarkMode.value ?? 'default');
|
||||
|
||||
if (!chartInstance) return;
|
||||
}
|
||||
clear && chartInstance?.clear();
|
||||
|
||||
chartInstance?.setOption(unref(getOptions));
|
||||
}, 30);
|
||||
});
|
||||
}
|
||||
|
||||
function resize() {
|
||||
chartInstance?.resize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册地图数据
|
||||
* @param {string} mapName - 地图名称
|
||||
* @param {object} geoJSON - GeoJSON 数据
|
||||
*/
|
||||
function registerMap(mapName, geoJSON) {
|
||||
if (!mapName || !geoJSON) {
|
||||
console.warn('地图名称或 GeoJSON 数据无效');
|
||||
return;
|
||||
}
|
||||
echarts.registerMap(mapName, geoJSON);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => getDarkMode.value,
|
||||
(theme) => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
initCharts(theme);
|
||||
setOptions(cacheOptions.value);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
tryOnUnmounted(() => {
|
||||
if (!chartInstance) return;
|
||||
removeResizeFn();
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
});
|
||||
|
||||
function getInstance() {
|
||||
if (!chartInstance) {
|
||||
initCharts(getDarkMode.value ?? 'default');
|
||||
}
|
||||
return chartInstance;
|
||||
}
|
||||
|
||||
return {
|
||||
setOptions,
|
||||
resize,
|
||||
echarts,
|
||||
getInstance,
|
||||
registerMap,
|
||||
};
|
||||
};
|
38
src/hooks/useEventListener.js
Normal file
38
src/hooks/useEventListener.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { ref, watch, unref } from 'vue';
|
||||
import { useThrottleFn, useDebounceFn } from '@vueuse/core';
|
||||
|
||||
export function useEventListener({ el = window, name, listener, options, autoRemove = true, isDebounce = true, wait = 80 }) {
|
||||
let remove;
|
||||
const isAddRef = ref(false);
|
||||
|
||||
if (el) {
|
||||
const element = ref(el);
|
||||
|
||||
const handler = isDebounce ? useDebounceFn(listener, wait) : useThrottleFn(listener, wait);
|
||||
const realHandler = wait ? handler : listener;
|
||||
const removeEventListener = (e) => {
|
||||
isAddRef.value = true;
|
||||
e.removeEventListener(name, realHandler, options);
|
||||
};
|
||||
const addEventListener = (e) => e.addEventListener(name, realHandler, options);
|
||||
|
||||
const removeWatch = watch(
|
||||
element,
|
||||
(v, _ov, cleanUp) => {
|
||||
if (v) {
|
||||
!unref(isAddRef) && addEventListener(v);
|
||||
cleanUp(() => {
|
||||
autoRemove && removeEventListener(v);
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
remove = () => {
|
||||
removeEventListener(element.value);
|
||||
removeWatch();
|
||||
};
|
||||
}
|
||||
return { removeEvent: remove };
|
||||
}
|
44
src/hooks/useTimeout.js
Normal file
44
src/hooks/useTimeout.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { ref, watch } from 'vue';
|
||||
import { tryOnUnmounted } from '@vueuse/core';
|
||||
|
||||
export function useTimeoutFn(handle, wait, native = false) {
|
||||
if (typeof handle !== 'function') {
|
||||
throw new Error('handle is not Function!');
|
||||
}
|
||||
|
||||
const { readyRef, stop, start } = useTimeoutRef(wait);
|
||||
if (native) {
|
||||
handle();
|
||||
} else {
|
||||
watch(
|
||||
readyRef,
|
||||
(maturity) => {
|
||||
maturity && handle();
|
||||
},
|
||||
{ immediate: false }
|
||||
);
|
||||
}
|
||||
return { readyRef, stop, start };
|
||||
}
|
||||
|
||||
export function useTimeoutRef(wait) {
|
||||
const readyRef = ref(false);
|
||||
|
||||
let timer;
|
||||
function stop() {
|
||||
readyRef.value = false;
|
||||
timer && window.clearTimeout(timer);
|
||||
}
|
||||
function start() {
|
||||
stop();
|
||||
timer = setTimeout(() => {
|
||||
readyRef.value = true;
|
||||
}, wait);
|
||||
}
|
||||
|
||||
start();
|
||||
|
||||
tryOnUnmounted(stop);
|
||||
|
||||
return { readyRef, stop, start };
|
||||
}
|
22
src/hooks/useWrapComponents.js
Normal file
22
src/hooks/useWrapComponents.js
Normal file
@ -0,0 +1,22 @@
|
||||
import { h } from 'vue';
|
||||
|
||||
const wrapperMap = new Map();
|
||||
|
||||
export const useWrapComponents = (Component, route) => {
|
||||
let wrapper;
|
||||
if (Component) {
|
||||
const wrapperName = route.name;
|
||||
if (wrapperMap.has(wrapperName)) {
|
||||
wrapper = wrapperMap.get(wrapperName);
|
||||
} else {
|
||||
wrapper = {
|
||||
name: wrapperName,
|
||||
render() {
|
||||
return h('div', { className: 'layout' }, Component);
|
||||
},
|
||||
};
|
||||
wrapperMap.set(wrapperName, wrapper);
|
||||
}
|
||||
return h(wrapper);
|
||||
}
|
||||
};
|
4
src/layouts/Views.vue
Normal file
4
src/layouts/Views.vue
Normal file
@ -0,0 +1,4 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
<script setup name="layout-views"></script>
|
3
src/layouts/index.vue
Normal file
3
src/layouts/index.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
16
src/main.js
Normal file
16
src/main.js
Normal file
@ -0,0 +1,16 @@
|
||||
import 'virtual:svg-icons-register';
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import pinia from './store';
|
||||
import ElementPlus from 'element-plus';
|
||||
import 'element-plus/dist/index.css';
|
||||
import 'animate.css';
|
||||
import './utils/permission';
|
||||
import { registerDirective } from './directives';
|
||||
import { registerElIcons } from './plugins/icon';
|
||||
|
||||
const app = createApp(App);
|
||||
registerElIcons(app);
|
||||
registerDirective(app);
|
||||
app.use(pinia).use(router).use(ElementPlus).mount('#app');
|
8
src/plugins/icon.js
Normal file
8
src/plugins/icon.js
Normal file
@ -0,0 +1,8 @@
|
||||
import * as ElIconsModules from '@element-plus/icons-vue';
|
||||
|
||||
// 全局注册element-plus icon图标组件
|
||||
export const registerElIcons = (app) => {
|
||||
Object.keys(ElIconsModules).forEach((key) => {
|
||||
app.component(key, ElIconsModules[key]);
|
||||
});
|
||||
};
|
51
src/router/index.js
Normal file
51
src/router/index.js
Normal file
@ -0,0 +1,51 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
// import Layout from '@/layouts/index.vue';
|
||||
|
||||
import demoRouters from './modules/demo';
|
||||
|
||||
export const constantRoutes = [
|
||||
// {
|
||||
// path: '/404',
|
||||
// name: '404',
|
||||
// component: () => import('@/views/error/404.vue'),
|
||||
// hidden: true,
|
||||
// },
|
||||
// {
|
||||
// path: '/403',
|
||||
// name: '403',
|
||||
// component: () => import('@/views/error/403.vue'),
|
||||
// hidden: true,
|
||||
// },
|
||||
// {
|
||||
// path: '/',
|
||||
// name: 'layout',
|
||||
// component: Layout,
|
||||
// redirect: '/home',
|
||||
// meta: { title: '首页', icon: 'House' },
|
||||
// children: [
|
||||
// {
|
||||
// path: '/home',
|
||||
// component: () => import('@/views/home/index.vue'),
|
||||
// name: 'home',
|
||||
// meta: { title: '首页', icon: 'House' },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
...demoRouters,
|
||||
];
|
||||
|
||||
/**
|
||||
* @Title notFoundRouter(找不到路由)
|
||||
*/
|
||||
export const notFoundRouter = {
|
||||
path: '/:pathMatch(.*)',
|
||||
name: 'notFound',
|
||||
redirect: '/404',
|
||||
};
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: constantRoutes,
|
||||
});
|
||||
|
||||
export default router;
|
14
src/router/modules/demo.js
Normal file
14
src/router/modules/demo.js
Normal file
@ -0,0 +1,14 @@
|
||||
export default [
|
||||
{
|
||||
path: '/scrollBoard',
|
||||
name: 'scrollBoard',
|
||||
component: () => import('@/views/demo/scrollBoard.vue'),
|
||||
meta: { title: '轮播列表', icon: 'document' },
|
||||
},
|
||||
{
|
||||
path: '/rank',
|
||||
name: 'rank',
|
||||
component: () => import('@/views/demo/rank.vue'),
|
||||
meta: { title: '滚动排名列表', icon: 'document' },
|
||||
},
|
||||
];
|
20
src/store/index.js
Normal file
20
src/store/index.js
Normal file
@ -0,0 +1,20 @@
|
||||
import { defineStore, createPinia } from 'pinia';
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||
|
||||
const { VITE_APP_NAME } = import.meta.env;
|
||||
|
||||
export const Store = defineStore({
|
||||
id: VITE_APP_NAME,
|
||||
state: () => ({}),
|
||||
getters: {},
|
||||
actions: {},
|
||||
persist: {
|
||||
key: VITE_APP_NAME,
|
||||
storage: window.sessionStorage,
|
||||
},
|
||||
});
|
||||
|
||||
const pinia = createPinia();
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
|
||||
export default pinia;
|
53
src/store/modules/permission.js
Normal file
53
src/store/modules/permission.js
Normal file
@ -0,0 +1,53 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { constantRoutes, notFoundRouter } from '@/router';
|
||||
import { createAsyncRoutes, filterAsyncRoutes, filterKeepAlive } from '@/utils/router';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { getTree } from '@/utils';
|
||||
import { GenKey } from '@/config';
|
||||
|
||||
export const usePermissionStore = defineStore({
|
||||
id: GenKey('permissionStore'),
|
||||
state: () => ({
|
||||
// 路由
|
||||
routes: [],
|
||||
// 动态路由
|
||||
asyncRoutes: [],
|
||||
// 缓存路由
|
||||
cacheRoutes: {},
|
||||
}),
|
||||
getters: {
|
||||
permissionRoutes: (state) => {
|
||||
return state.routes;
|
||||
},
|
||||
keepAliveRoutes: (state) => {
|
||||
return filterKeepAlive(state.asyncRoutes);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
generateRoutes(roles) {
|
||||
return new Promise((resolve) => {
|
||||
// 在这判断是否有权限,哪些角色拥有哪些权限
|
||||
const UserStore = useUserStore();
|
||||
this.asyncRoutes = createAsyncRoutes(getTree(UserStore.getMenus()));
|
||||
let accessedRoutes;
|
||||
if (roles && roles.length && !roles.includes('admin')) {
|
||||
accessedRoutes = filterAsyncRoutes(this.asyncRoutes, roles);
|
||||
} else {
|
||||
accessedRoutes = this.asyncRoutes || [];
|
||||
}
|
||||
accessedRoutes = accessedRoutes.concat(notFoundRouter);
|
||||
this.routes = constantRoutes.concat(accessedRoutes);
|
||||
resolve(accessedRoutes);
|
||||
});
|
||||
},
|
||||
clearRoutes() {
|
||||
this.routes = [];
|
||||
this.asyncRoutes = [];
|
||||
this.cacheRoutes = [];
|
||||
},
|
||||
getCacheRoutes() {
|
||||
this.cacheRoutes = filterKeepAlive(this.asyncRoutes);
|
||||
return this.cacheRoutes;
|
||||
},
|
||||
},
|
||||
});
|
70
src/store/modules/setting.js
Normal file
70
src/store/modules/setting.js
Normal file
@ -0,0 +1,70 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { CONSTANTS } from '@/config';
|
||||
import { GenKey } from '@/config';
|
||||
|
||||
export const useSettingStore = defineStore({
|
||||
id: GenKey('settingStore'),
|
||||
state: () => ({
|
||||
// menu 是否收缩
|
||||
isCollapse: true,
|
||||
//
|
||||
withoutAnimation: false,
|
||||
device: 'desktop',
|
||||
// 刷新当前页
|
||||
isReload: true,
|
||||
// 主题设置
|
||||
themeConfig: {
|
||||
// 显示设置
|
||||
showSetting: false,
|
||||
// 菜单展示模式 默认 vertical horizontal / vertical /columns
|
||||
mode: 'vertical',
|
||||
// tagsView 是否展示 默认展示
|
||||
showTag: true,
|
||||
// 页脚
|
||||
footer: true,
|
||||
// 深色模式 切换暗黑模式
|
||||
isDark: false,
|
||||
// 显示侧边栏Logo
|
||||
showLogo: true,
|
||||
// 主题颜色
|
||||
primary: CONSTANTS.PRIMARY,
|
||||
// element组件大小
|
||||
globalComSize: 'default',
|
||||
// 是否只保持一个子菜单的展开
|
||||
uniqueOpened: true,
|
||||
// 固定header
|
||||
fixedHeader: true,
|
||||
// 灰色模式
|
||||
gray: false,
|
||||
// 色弱模式
|
||||
weak: false,
|
||||
},
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
// 设置主题
|
||||
setThemeConfig({ key, val }) {
|
||||
this.themeConfig[key] = val;
|
||||
},
|
||||
// 切换 Collapse
|
||||
setCollapse(value) {
|
||||
this.isCollapse = value;
|
||||
this.withoutAnimation = false;
|
||||
},
|
||||
// 关闭侧边栏
|
||||
closeSideBar({ withoutAnimation }) {
|
||||
this.isCollapse = false;
|
||||
this.withoutAnimation = withoutAnimation;
|
||||
},
|
||||
toggleDevice(device) {
|
||||
this.device = device;
|
||||
},
|
||||
// 刷新
|
||||
setReload() {
|
||||
this.isReload = false;
|
||||
setTimeout(() => {
|
||||
this.isReload = true;
|
||||
}, 50);
|
||||
},
|
||||
},
|
||||
});
|
105
src/store/modules/tagsView.js
Normal file
105
src/store/modules/tagsView.js
Normal file
@ -0,0 +1,105 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { GenKey } from '@/config';
|
||||
import router from '@/router';
|
||||
|
||||
export const useTagsViewStore = defineStore({
|
||||
id: GenKey('tagsViewStore'),
|
||||
state: () => ({
|
||||
activeTabsValue: '/home',
|
||||
visitedViews: [],
|
||||
cachedViews: [],
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
setTabsMenuValue(val) {
|
||||
this.activeTabsValue = val;
|
||||
},
|
||||
addView(view) {
|
||||
this.addVisitedView(view);
|
||||
},
|
||||
removeView(routes) {
|
||||
return new Promise((resolve) => {
|
||||
this.visitedViews = this.visitedViews.filter((item) => !routes.includes(item.path));
|
||||
resolve(null);
|
||||
});
|
||||
},
|
||||
addVisitedView(view) {
|
||||
this.setTabsMenuValue(view.path);
|
||||
if (this.visitedViews.some((v) => v.path === view.path)) return;
|
||||
|
||||
this.visitedViews.push(
|
||||
Object.assign({}, view, {
|
||||
title: view.meta.title || 'no-name',
|
||||
})
|
||||
);
|
||||
if (view.meta.keepAlive) {
|
||||
this.cachedViews.push(view.name);
|
||||
}
|
||||
},
|
||||
delView(activeTabPath) {
|
||||
return new Promise((resolve) => {
|
||||
this.delVisitedView(activeTabPath);
|
||||
this.delCachedView(activeTabPath);
|
||||
resolve({
|
||||
visitedViews: [...this.visitedViews],
|
||||
cachedViews: [...this.cachedViews],
|
||||
});
|
||||
});
|
||||
},
|
||||
toLastView(activeTabPath) {
|
||||
const index = this.visitedViews.findIndex((item) => item.path === activeTabPath);
|
||||
const nextTab = this.visitedViews[index + 1] || this.visitedViews[index - 1];
|
||||
if (!nextTab) return;
|
||||
router.push(nextTab.path);
|
||||
this.addVisitedView(nextTab);
|
||||
},
|
||||
delVisitedView(path) {
|
||||
return new Promise((resolve) => {
|
||||
this.visitedViews = this.visitedViews.filter((v) => {
|
||||
return v.path !== path || v.meta.affix;
|
||||
});
|
||||
this.cachedViews = this.cachedViews.filter((v) => {
|
||||
return v.path !== path || v.meta.affix;
|
||||
});
|
||||
resolve([...this.visitedViews]);
|
||||
});
|
||||
},
|
||||
delCachedView(view) {
|
||||
return new Promise((resolve) => {
|
||||
const index = this.cachedViews.indexOf(view.name);
|
||||
index > -1 && this.cachedViews.splice(index, 1);
|
||||
resolve([...this.cachedViews]);
|
||||
});
|
||||
},
|
||||
clearVisitedView() {
|
||||
this.delAllViews();
|
||||
},
|
||||
delAllViews() {
|
||||
return new Promise((resolve) => {
|
||||
this.visitedViews = this.visitedViews.filter((v) => v.meta.affix);
|
||||
this.cachedViews = this.visitedViews.filter((v) => v.meta.affix);
|
||||
resolve([...this.visitedViews]);
|
||||
});
|
||||
},
|
||||
delOtherViews(path) {
|
||||
this.visitedViews = this.visitedViews.filter((item) => {
|
||||
return item.path === path || item.meta.affix;
|
||||
});
|
||||
this.cachedViews = this.visitedViews.filter((item) => {
|
||||
return item.path === path || item.meta.affix;
|
||||
});
|
||||
},
|
||||
goHome() {
|
||||
this.activeTabsValue = '/home';
|
||||
router.push({ path: '/home' });
|
||||
},
|
||||
updateVisitedView(view) {
|
||||
for (let v of this.visitedViews) {
|
||||
if (v.path === view.path) {
|
||||
v = Object.assign(v, view);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
65
src/store/modules/user.js
Normal file
65
src/store/modules/user.js
Normal file
@ -0,0 +1,65 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { GenKey } from '@/config';
|
||||
import { isEmpty, encode, decode } from '@/utils';
|
||||
|
||||
export const useUserStore = defineStore({
|
||||
id: GenKey('userStore'),
|
||||
state: () => ({
|
||||
token: null,
|
||||
userInfo: {},
|
||||
currentOrg: null,
|
||||
orgList: [],
|
||||
menus: [],
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
setToken(token) {
|
||||
this.token = token;
|
||||
},
|
||||
hasToken() {
|
||||
return !isEmpty(this.token);
|
||||
},
|
||||
setUserInfo(userInfo) {
|
||||
this.userInfo = encode(JSON.stringify(userInfo), true);
|
||||
},
|
||||
getUserInfo() {
|
||||
return !isEmpty(this.userInfo) ? JSON.parse(decode(this.userInfo, true)) : {};
|
||||
},
|
||||
setOrgList(orgList) {
|
||||
this.orgList = encode(JSON.stringify(orgList), true);
|
||||
},
|
||||
getOrgList() {
|
||||
return !isEmpty(this.orgList) ? JSON.parse(decode(this.orgList, true)) : [];
|
||||
},
|
||||
setCurrentOrg(org) {
|
||||
this.currentOrg = org;
|
||||
},
|
||||
getCurrentOrg() {
|
||||
const list = this.getOrgList().filter((item) => {
|
||||
return item.id === this.currentOrg;
|
||||
});
|
||||
return !isEmpty(list) ? list[0] : {};
|
||||
},
|
||||
setMenus(menus) {
|
||||
this.menus = encode(JSON.stringify(menus), true);
|
||||
},
|
||||
getMenus() {
|
||||
return !isEmpty(this.menus) ? JSON.parse(decode(this.menus, true)) : [];
|
||||
},
|
||||
logout() {
|
||||
this.token = null;
|
||||
this.userInfo = {};
|
||||
this.currentOrg = null;
|
||||
this.orgList = [];
|
||||
this.menus = [];
|
||||
localStorage.removeItem(GenKey('userStore'));
|
||||
},
|
||||
clear() {
|
||||
localStorage.removeItem(GenKey('userStore'));
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
key: GenKey('userStore'),
|
||||
storage: window.localStorage,
|
||||
},
|
||||
});
|
283
src/styles/common/base.scss
Normal file
283
src/styles/common/base.scss
Normal file
@ -0,0 +1,283 @@
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
a,
|
||||
abbr,
|
||||
acronym,
|
||||
address,
|
||||
article,
|
||||
aside,
|
||||
blockquote,
|
||||
caption,
|
||||
code,
|
||||
del,
|
||||
dfn,
|
||||
dialog,
|
||||
header,
|
||||
footer,
|
||||
nav,
|
||||
object,
|
||||
section,
|
||||
body,
|
||||
dd,
|
||||
div,
|
||||
dl,
|
||||
dt,
|
||||
em,
|
||||
img,
|
||||
fieldset,
|
||||
figure,
|
||||
form,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hgroup,
|
||||
iframe,
|
||||
legend,
|
||||
p,
|
||||
pre,
|
||||
q,
|
||||
span,
|
||||
tbody,
|
||||
tfoot,
|
||||
thead,
|
||||
ul,
|
||||
ol,
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
list-style: none;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
dialog,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Hiragino Sans GB', 'Heiti SC', 'Microsoft YaHei', 'WenQuanYi Micro Hei', sans-serif;
|
||||
color: #323232;
|
||||
|
||||
// background: #000;
|
||||
}
|
||||
img {
|
||||
vertical-align: bottom;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
// ::input-placeholder {
|
||||
// color: #999999;
|
||||
// }
|
||||
::placeholder {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
// :input-placeholder {
|
||||
// color: #cccccc;
|
||||
// }
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
button:focus,
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: 0;
|
||||
}
|
||||
input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
audio,
|
||||
canvas,
|
||||
progress,
|
||||
video {
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
[hidden],
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #323232;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
a:focus {
|
||||
outline: thin dotted;
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
b,
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
mark {
|
||||
color: #000000;
|
||||
background: #ffff00;
|
||||
}
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
}
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-size: 1em;
|
||||
font-family: monospace;
|
||||
}
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
}
|
||||
button {
|
||||
overflow: visible;
|
||||
}
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
button,
|
||||
html input[type='button'],
|
||||
input[type='reset'],
|
||||
input[type='submit'] {
|
||||
appearance: button;
|
||||
cursor: pointer;
|
||||
}
|
||||
button[disabled],
|
||||
html input[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
input {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: normal;
|
||||
}
|
||||
input[type='checkbox'],
|
||||
input[type='radio'] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
input[type='search'] {
|
||||
box-sizing: content-box;
|
||||
appearance: textfield;
|
||||
}
|
||||
input[type='search']::-webkit-search-cancel-button,
|
||||
input[type='search']::-webkit-search-decoration {
|
||||
appearance: none;
|
||||
}
|
||||
fieldset {
|
||||
margin: 0 2px;
|
||||
padding: 0.35em 0.625em 0.75em;
|
||||
border: 1px solid silver;
|
||||
}
|
||||
legend {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
optgroup {
|
||||
font-weight: 700;
|
||||
}
|
||||
table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
td,
|
||||
th {
|
||||
padding: 0;
|
||||
}
|
10
src/styles/common/define.scss
Normal file
10
src/styles/common/define.scss
Normal file
@ -0,0 +1,10 @@
|
||||
.flex {
|
||||
&-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
&-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
61
src/styles/global.scss
Normal file
61
src/styles/global.scss
Normal file
@ -0,0 +1,61 @@
|
||||
// color
|
||||
$legacy-ie: 10;
|
||||
$color-primary: #20a0ff;
|
||||
$color-success: #13ce66;
|
||||
$color-warning: #f7ba2a;
|
||||
$color-danger: #ff4949;
|
||||
$color-info: #50bfff;
|
||||
$color-secondary: #2e90fe;
|
||||
$color-white: #ffffff;
|
||||
$color-black: #1f2d3d;
|
||||
$color-black-light: #324057;
|
||||
$color-black-lighter: #475669;
|
||||
$color-blue-light: #5da9ff;
|
||||
$color-blue-lighter: #5da9ff;
|
||||
$color-black: #000000;
|
||||
$color-silver: #8492a6;
|
||||
$color-silver-light: #99a9bf;
|
||||
$color-silver-lighter: #c0ccda;
|
||||
$color-gray: #d3dce6;
|
||||
$color-gray-light: #e5e9f2;
|
||||
$color-gray-lighter: #eff2f7;
|
||||
$color-333: #333333;
|
||||
$color-666: #666666;
|
||||
$color-999: #999999;
|
||||
$color-border-gray: #d1dbe5;
|
||||
$color-input-border: #dcdfe6;
|
||||
$color-border: $color-border-gray;
|
||||
$color-types: (
|
||||
primary: (
|
||||
$color-primary,
|
||||
#4db3ff,
|
||||
#1d90e6,
|
||||
),
|
||||
info: (
|
||||
$color-info,
|
||||
#73ccff,
|
||||
#48ace6,
|
||||
),
|
||||
success: (
|
||||
$color-success,
|
||||
#42d885,
|
||||
#11b95c,
|
||||
),
|
||||
warning: (
|
||||
$color-warning,
|
||||
#f9c855,
|
||||
#dea726,
|
||||
),
|
||||
danger: (
|
||||
$color-danger,
|
||||
#ff6d6d,
|
||||
#e64242,
|
||||
),
|
||||
gray: (
|
||||
$color-999,
|
||||
#999999,
|
||||
#9d9d9d,
|
||||
),
|
||||
);
|
||||
|
||||
@import 'utils/utils';
|
17
src/styles/style.scss
Normal file
17
src/styles/style.scss
Normal file
@ -0,0 +1,17 @@
|
||||
@import 'common/base';
|
||||
@import 'common/define';
|
||||
|
||||
#app {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: Avenir, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #fff;
|
||||
}
|
||||
// .el-dialog__body {
|
||||
// overflow: hidden auto;
|
||||
// height: auto;
|
||||
// max-height: calc(100vh - 130px);
|
||||
// }
|
31
src/styles/utils/_bem.scss
Normal file
31
src/styles/utils/_bem.scss
Normal file
@ -0,0 +1,31 @@
|
||||
/// Block Element
|
||||
/// @access public
|
||||
/// @param {String} $element - Element's name
|
||||
@mixin element($element) {
|
||||
&__#{$element} {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
/// @alias element
|
||||
@mixin e($element) {
|
||||
@include element($element) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
/// Block Modifier
|
||||
/// @access public
|
||||
/// @param {String} $modifier - Modifier's name
|
||||
@mixin modifier($modifier) {
|
||||
&_#{$modifier} {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
/// @alias modifier
|
||||
@mixin m($modifier) {
|
||||
@include modifier($modifier) {
|
||||
@content;
|
||||
}
|
||||
}
|
30
src/styles/utils/_ellipsis.scss
Normal file
30
src/styles/utils/_ellipsis.scss
Normal file
@ -0,0 +1,30 @@
|
||||
@mixin ellipsis($lines: 1, $line-height: 0) {
|
||||
overflow: hidden;
|
||||
|
||||
@if $lines == 1 {
|
||||
@if $legacy-ie <= 8 {
|
||||
word-wrap: normal; // for ie
|
||||
}
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
} @else {
|
||||
display: flexbox;
|
||||
-webkit-line-clamp: $lines;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
@if value-of($line-height) == 0 {
|
||||
@error 'line-height is required when clamp muti lines';
|
||||
}
|
||||
|
||||
@if unitless($line-height) or unit($line-height) == '%' {
|
||||
$line-height: value-of($line-height) * 1em;
|
||||
}
|
||||
|
||||
max-height: $line-height * $lines;
|
||||
|
||||
// &:after {
|
||||
// content: " ...";
|
||||
// }
|
||||
}
|
||||
}
|
4
src/styles/utils/_scrollable.scss
Normal file
4
src/styles/utils/_scrollable.scss
Normal file
@ -0,0 +1,4 @@
|
||||
@mixin scrollable() {
|
||||
overflow: hidden auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
32
src/styles/utils/_scrollbar.scss
Normal file
32
src/styles/utils/_scrollbar.scss
Normal file
@ -0,0 +1,32 @@
|
||||
/// Mixin to customize scrollbars
|
||||
/// Beware, this does not work in all browsers
|
||||
/// @author Hugo Giraudel
|
||||
/// @param {Length} $size - Horizontal scrollbar's height and vertical scrollbar's width
|
||||
/// @param {Color} $foreground-color - Scrollbar's color
|
||||
/// @param {Color} $background-color [mix($foreground-color, white, 50%)] - Scrollbar's color
|
||||
/// @example scss - Scrollbar styling
|
||||
/// @include scrollbars(.5em, slategray);
|
||||
@mixin scrollbars($size, $foreground-color, $background-color: mix($foreground-color, white, 50%)) {
|
||||
// For Google Chrome
|
||||
::-webkit-scrollbar {
|
||||
width: $size;
|
||||
height: $size;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: $foreground-color;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: $background-color;
|
||||
}
|
||||
|
||||
// For Internet Explorer
|
||||
body {
|
||||
scrollbar-face-color: $foreground-color;
|
||||
scrollbar-track-color: $background-color;
|
||||
}
|
||||
}
|
||||
|
||||
/// alias
|
||||
@mixin scrollbar($size, $foreground-color, $background-color: mix($foreground-color, white, 50%)) {
|
||||
@include scrollbars($size, $foreground-color, $background-color: mix($foreground-color, white, 50%));
|
||||
}
|
11
src/styles/utils/_value-of.scss
Normal file
11
src/styles/utils/_value-of.scss
Normal file
@ -0,0 +1,11 @@
|
||||
@function value-of($value) {
|
||||
@if type-of($value) == 'number' and not unitless($value) {
|
||||
@return $value / ($value * 0 + 1);
|
||||
}
|
||||
@return $value;
|
||||
}
|
||||
|
||||
// alias
|
||||
@function strip-unit($value) {
|
||||
@return value-of($value);
|
||||
}
|
21
src/styles/utils/utils.scss
Normal file
21
src/styles/utils/utils.scss
Normal file
@ -0,0 +1,21 @@
|
||||
@import 'bem';
|
||||
@import 'ellipsis';
|
||||
@import 'scrollable';
|
||||
@import 'scrollbar';
|
||||
@import 'value-of';
|
||||
|
||||
@mixin flex-row() {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@mixin flex-column() {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@mixin icon-space() {
|
||||
display: inline-block;
|
||||
content: '';
|
||||
background-size: 100%;
|
||||
}
|
123
src/utils/axios.js
Normal file
123
src/utils/axios.js
Normal file
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* @Descripttion:
|
||||
* @Author: zenghua.wang
|
||||
* @Date: 2022-02-23 21:12:37
|
||||
* @LastEditors: zenghua.wang
|
||||
* @LastEditTime: 2025-02-18 09:47:41
|
||||
*/
|
||||
import axios from 'axios';
|
||||
import { ElNotification } from 'element-plus';
|
||||
import router from '@/router';
|
||||
import { isEmpty } from '@/utils';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
const { VITE_APP_BASE_API, VITE_APP_UPLOAD_API, VITE_APP_DICDATA_API } = import.meta.env;
|
||||
|
||||
/**
|
||||
* 创建axios实例
|
||||
*/
|
||||
const publicAxios = axios.create({
|
||||
baseURL: VITE_APP_BASE_API, // API请求的默认前缀
|
||||
timeout: 30000,
|
||||
});
|
||||
/**
|
||||
* 异常拦截处理器
|
||||
* @param error
|
||||
* @returns
|
||||
*/
|
||||
const errorHandler = async (error) => {
|
||||
const { response } = error;
|
||||
const UserStore = useUserStore();
|
||||
if (response && response.status) {
|
||||
switch (response.status) {
|
||||
case 401:
|
||||
await UserStore.logout();
|
||||
router.push('/login');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Promise.reject(error?.response?.data);
|
||||
};
|
||||
/**
|
||||
* 请求拦截器
|
||||
*/
|
||||
publicAxios.interceptors.request.use(async (config) => {
|
||||
const UserStore = useUserStore();
|
||||
switch (config.apisType) {
|
||||
case 'upload': {
|
||||
config.baseURL = VITE_APP_UPLOAD_API;
|
||||
config.headers['Content-Type'] = config.uploadType;
|
||||
break;
|
||||
}
|
||||
case 'dicData': {
|
||||
config.baseURL = VITE_APP_DICDATA_API;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
config.baseURL = VITE_APP_BASE_API;
|
||||
}
|
||||
}
|
||||
if (UserStore.hasToken()) {
|
||||
config.headers['authorization'] = config.headers['authorization'] ?? UserStore.token;
|
||||
config.headers['cache-control'] = 'no-cache';
|
||||
config.headers.Pragma = 'no-cache';
|
||||
}
|
||||
if (config.method === 'POST' || config.method === 'DELETE') {
|
||||
config.headers.Accept = 'application/json';
|
||||
}
|
||||
return config;
|
||||
}, errorHandler);
|
||||
|
||||
/**
|
||||
* 返回结果处理
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
const formatResult = async (res) => {
|
||||
const code = res.data.code || res.status;
|
||||
// console.info('formatResult', code)
|
||||
const UserStore = useUserStore();
|
||||
switch (code) {
|
||||
case 200:
|
||||
case 0:
|
||||
// code === 0 或 200 代表没有错误
|
||||
return res.data || res.data.data || res;
|
||||
case 500:
|
||||
case 1:
|
||||
// code === 1 或 500 代表存在错误
|
||||
ElNotification.error(res.data.msg);
|
||||
break;
|
||||
case 401:
|
||||
// code === 401 代表登录过期
|
||||
await UserStore.logout();
|
||||
router.push('/login');
|
||||
break;
|
||||
default:
|
||||
ElNotification.error(res.data.msg);
|
||||
break;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 响应拦截器
|
||||
*/
|
||||
publicAxios.interceptors.response.use((response) => {
|
||||
const { config } = response;
|
||||
// console.info('响应拦截器', response);
|
||||
if (config?.responseType) {
|
||||
return response;
|
||||
}
|
||||
const token = response?.headers['authorization'];
|
||||
if (!isEmpty(token)) {
|
||||
const UserStore = useUserStore();
|
||||
UserStore.setToken(token);
|
||||
}
|
||||
const result = formatResult(response);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
throw new Error(response.data.msg);
|
||||
}, errorHandler);
|
||||
|
||||
export default publicAxios;
|
483
src/utils/index.js
Normal file
483
src/utils/index.js
Normal file
@ -0,0 +1,483 @@
|
||||
/*
|
||||
* @Descripttion:
|
||||
* @Author: zenghua.wang
|
||||
* @Date: 2022-02-23 21:12:37
|
||||
* @LastEditors: zenghua.wang
|
||||
* @LastEditTime: 2025-02-11 17:18:36
|
||||
*/
|
||||
import lodash from 'lodash';
|
||||
import dayjs from 'dayjs';
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
/**
|
||||
* @Title 防抖:指在一定时间内,多次触发同一个事件,只执行最后一次操作
|
||||
* @param {*} fn
|
||||
* @param {*} delay
|
||||
* @returns
|
||||
*/
|
||||
export function debounce(fn, delay) {
|
||||
let timer = null;
|
||||
return function (...args) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
fn.apply(this, args);
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
/**
|
||||
* @Title 节流:指在一定时间内,多次触发同一个事件,只执行第一次操作
|
||||
* @param {*} fn
|
||||
* @param {*} delay
|
||||
* @returns
|
||||
*/
|
||||
export function throttle(fn, delay) {
|
||||
let timer = null;
|
||||
return function (...args) {
|
||||
if (!timer) {
|
||||
timer = setTimeout(() => {
|
||||
fn.apply(this, args);
|
||||
timer = null;
|
||||
}, delay);
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* @Title 判断是否 empty,返回ture
|
||||
* @param {*} val:null 'null' undefined 'undefined' 0 '0' "" 返回true
|
||||
* @returns
|
||||
*/
|
||||
export const isEmpty = (val) => {
|
||||
if (val && parseInt(val) === 0) return false;
|
||||
if (typeof val === 'undefined' || val === 'null' || val == null || val === 'undefined' || val === undefined || val === '') {
|
||||
return true;
|
||||
} else if (typeof val === 'object' && Object.keys(val).length === 0) {
|
||||
return true;
|
||||
} else if (val instanceof Array && val.length === 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* @Title 深度拷贝对象
|
||||
* @param {*} obj
|
||||
* @returns
|
||||
*/
|
||||
export const deepClone = (obj = {}) => {
|
||||
return lodash.cloneDeep(obj);
|
||||
};
|
||||
/**
|
||||
* @Title 将number转换为px
|
||||
* @param {*} val
|
||||
* @returns
|
||||
*/
|
||||
export const setPx = (val) => {
|
||||
if (isEmpty(val)) return '';
|
||||
val = val + '';
|
||||
if (val.indexOf('%') === -1) {
|
||||
val = val + 'px';
|
||||
}
|
||||
return val;
|
||||
};
|
||||
/**
|
||||
* @Tilte 设置属性默认值
|
||||
* @param {*} options
|
||||
* @param {*} prop
|
||||
* @param {*} defaultVal
|
||||
* @returns
|
||||
*/
|
||||
export const setDefaultOption = (options, prop, defaultVal) => {
|
||||
return options[prop] === undefined ? defaultVal : options.prop;
|
||||
};
|
||||
/**
|
||||
* @Title 设置字典值
|
||||
* @param {*} columns
|
||||
* @param {*} key
|
||||
* @param {*} data
|
||||
* @returns
|
||||
*/
|
||||
export const setDicData = (columns, key, data = []) => {
|
||||
if (isEmpty(data)) return;
|
||||
const len = columns.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (columns[i]?.prop === key) {
|
||||
columns[i]['dicData'] = data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
* @Title 求字段lable
|
||||
* @param {*} tree
|
||||
* @returns
|
||||
*/
|
||||
export const setDicLabel = (dicData, value) => {
|
||||
let label = value;
|
||||
if (isEmpty(dicData)) return label;
|
||||
const len = dicData.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (dicData[i]?.value === value) {
|
||||
label = dicData[i].label;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return label;
|
||||
};
|
||||
/**
|
||||
* @Title 数组交集
|
||||
* @param {*} arr1
|
||||
* @param {*} arr2
|
||||
* @returns
|
||||
*/
|
||||
export const intersectionArray = (arr1 = [], arr2 = []) => {
|
||||
return arr1.filter((item) => arr2.includes(item));
|
||||
};
|
||||
/**
|
||||
* @Title 数组并集
|
||||
* @param {*} arr1
|
||||
* @param {*} arr2
|
||||
* @returns
|
||||
*/
|
||||
export const unionArray = (arr1 = [], arr2 = []) => {
|
||||
return Array.from(new Set([...arr1, ...arr2]));
|
||||
};
|
||||
/**
|
||||
* @Title 数组差集
|
||||
* @param {*} arr1
|
||||
* @param {*} arr2
|
||||
* @returns
|
||||
*/
|
||||
export const differenceArray = (arr1 = [], arr2 = []) => {
|
||||
const s = new Set(arr2);
|
||||
return arr1.filter((x) => !s.has(x));
|
||||
};
|
||||
/**
|
||||
* @Title 加密
|
||||
* @param {*} n
|
||||
* @returns
|
||||
*/
|
||||
export const encode = (n, flag = false) => {
|
||||
if (flag) {
|
||||
return (
|
||||
((e) => {
|
||||
let t = e.length.toString();
|
||||
for (let n = 10 - t.length; n > 0; n--) t = '0' + t;
|
||||
return t;
|
||||
})(n) +
|
||||
((e) => {
|
||||
const t = Base64.encode(e).split('');
|
||||
for (let n = 0; n < Math.floor(e.length / 100 + 1); n++) t.splice(100 * n + 1, 0, 3);
|
||||
return t.join('');
|
||||
})(n)
|
||||
);
|
||||
}
|
||||
return n;
|
||||
};
|
||||
/**
|
||||
* @Title 解密
|
||||
* @param {*} e
|
||||
* @returns
|
||||
*/
|
||||
export const decode = (e, flag = false) => {
|
||||
if (flag) {
|
||||
try {
|
||||
const t = Number(e.substr(0, 10));
|
||||
const n = e.substr(10).split('');
|
||||
for (let i = 0, s = 0; s < Math.floor(t / 100) + 1; s++) {
|
||||
n.splice(100 * s + 1 - i, 1);
|
||||
i++;
|
||||
}
|
||||
const o = Base64.decode(n.join(''));
|
||||
return o;
|
||||
} catch (error) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return e;
|
||||
};
|
||||
/**
|
||||
* @Title 图片转base64
|
||||
* @param {*} file
|
||||
* @returns
|
||||
*/
|
||||
export const imageToBase64 = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => {
|
||||
resolve(reader.result);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
});
|
||||
};
|
||||
/**
|
||||
* @Title bufferToBase64
|
||||
* @param {*} buffer
|
||||
* @returns
|
||||
*/
|
||||
export const bufferToBase64 = (buffer) => {
|
||||
return 'data:image/jpeg;base64,' + window.btoa(new Uint8Array(buffer).reduce((data, byte) => data + String.fromCharCode(byte), ''));
|
||||
};
|
||||
/**
|
||||
* @Title blob转json
|
||||
* @param {*} file
|
||||
* @returns
|
||||
*/
|
||||
export const blobToJSON = (blob) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(blob, 'utf-8');
|
||||
reader.onload = () => {
|
||||
const res = !isEmpty(reader.result) ? JSON.parse(reader.result) : reader.result;
|
||||
resolve(res);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
});
|
||||
};
|
||||
/**
|
||||
* @Title 将array转化为树
|
||||
* @param tree
|
||||
* @returns
|
||||
*/
|
||||
export const getTree = (tree = []) => {
|
||||
tree.forEach((item) => {
|
||||
delete item.children;
|
||||
});
|
||||
const map = {};
|
||||
tree.forEach((item) => {
|
||||
map[item.id] = item;
|
||||
});
|
||||
const arr = [];
|
||||
tree.forEach((item) => {
|
||||
const parent = map[item.parentId];
|
||||
if (parent) {
|
||||
(parent.children || (parent.children = [])).push(item);
|
||||
} else {
|
||||
arr.push(item);
|
||||
}
|
||||
});
|
||||
return arr;
|
||||
};
|
||||
/**
|
||||
* @Title 获取路由中的参数
|
||||
* @param name
|
||||
* @returns
|
||||
*/
|
||||
export const getUrlQuery = (name) => {
|
||||
const url = window.location.href;
|
||||
const hash = url.substring(url.indexOf('#') + 1);
|
||||
const searchIndex = hash.indexOf('?');
|
||||
const search = searchIndex !== -1 ? hash.substring(searchIndex + 1) : '';
|
||||
const usp = new URLSearchParams(search);
|
||||
return usp.get(name);
|
||||
};
|
||||
/**
|
||||
* @Title 将Object参数转换为字符串
|
||||
* @param {*} json
|
||||
* @returns
|
||||
*/
|
||||
export const obj2Param = (json) => {
|
||||
if (!json) return '';
|
||||
return Object.keys(json)
|
||||
.map((key) => {
|
||||
if (isEmpty(json[key])) return '';
|
||||
return encodeURIComponent(key) + '=' + encodeURIComponent(json[key]);
|
||||
})
|
||||
.join('&');
|
||||
};
|
||||
/**
|
||||
* @Title 获取静态资源文件
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const getAssetsFile = (url) => {
|
||||
return new URL(`../assets/${url}`, import.meta.url);
|
||||
};
|
||||
/**
|
||||
* @Title 替换图片url字段值
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const setUploadField = (url) => {
|
||||
// if (isEmpty(url) || url.includes('http')) return null;
|
||||
return isEmpty(url) ? null : url;
|
||||
};
|
||||
/**
|
||||
* @Title: 下载文件
|
||||
* @param {void} url:
|
||||
* @param {void} fileName:
|
||||
* @param {void} fileType:
|
||||
* @return {void}
|
||||
*/
|
||||
export const downloadFile = async (url, fileName, fileType) => {
|
||||
let blob = null;
|
||||
try {
|
||||
switch (fileType) {
|
||||
case 'image': {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.src = url;
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = resolve;
|
||||
img.onerror = reject;
|
||||
});
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
blob = await new Promise((resolve) => {
|
||||
canvas.toBlob(resolve, 'image/jpeg');
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'blob':
|
||||
case 'arraybuffer': {
|
||||
blob = new Blob([url]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ('download' in document.createElement('a')) {
|
||||
const elink = document.createElement('a');
|
||||
elink.download = fileName;
|
||||
elink.style.display = 'none';
|
||||
elink.href = blob ? URL.createObjectURL(blob) : url;
|
||||
document.body.appendChild(elink);
|
||||
elink.click();
|
||||
blob && URL.revokeObjectURL(elink.href);
|
||||
document.body.removeChild(elink);
|
||||
} else {
|
||||
navigator.msSaveBlob(blob, fileName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载出错:', error);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* @Title 模拟休眠
|
||||
* @param {*} duration
|
||||
* @returns
|
||||
*/
|
||||
export const sleep = (duration = 0) =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, duration);
|
||||
});
|
||||
/**
|
||||
* @Title 创建id
|
||||
* @param {*} prefix
|
||||
* @returns
|
||||
*/
|
||||
export const createId = (prefix) => {
|
||||
const val = Date.now() + Math.ceil(Math.random() * 99999);
|
||||
return isEmpty(prefix) ? val : prefix + '-' + val;
|
||||
};
|
||||
/**
|
||||
* @Title 生成数据
|
||||
* @param {*} duration
|
||||
* @returns
|
||||
*/
|
||||
export const mockData = (item = {}, len = 1) => {
|
||||
const list = [];
|
||||
for (let i = 0; i < len; i++) {
|
||||
let temp = { ...item, id: createId() };
|
||||
list.push(temp);
|
||||
}
|
||||
return list;
|
||||
};
|
||||
/**
|
||||
* @Title 日期格式化
|
||||
* @param {*} date
|
||||
* @param {*} format
|
||||
* @returns
|
||||
*/
|
||||
export const dateFormat = (datetime, formater = 'YYYY-MM-DD hh:mm:ss') => {
|
||||
if (datetime instanceof Date || datetime) {
|
||||
return dayjs(datetime).format(formater);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* @Title 字符串转日期
|
||||
* @param {*} str
|
||||
* @returns
|
||||
*/
|
||||
export const toDate = (str) => {
|
||||
return !isEmpty(str) ? dayjs(str) : dayjs();
|
||||
};
|
||||
/**
|
||||
* @Title 字符串转日期
|
||||
* @param {*} str
|
||||
* @returns
|
||||
*/
|
||||
export const getDate = (num, type, formater = 'YYYY-MM-DD', start = true) => {
|
||||
const date = dayjs().subtract(num, type);
|
||||
return start ? date.startOf(type).format(formater) : date.endOf(type).format(formater);
|
||||
};
|
||||
/**
|
||||
* @Title: 获取时间差
|
||||
* @param start
|
||||
* @param end
|
||||
* @param type
|
||||
* @returns
|
||||
*/
|
||||
export const getDiffTime = (start, end, type) => {
|
||||
const startTime = dayjs(start);
|
||||
const endTime = dayjs(end);
|
||||
const duration = endTime.diff(startTime);
|
||||
let diff = 0;
|
||||
switch (type) {
|
||||
case 'DD': {
|
||||
diff = duration / (1000 * 60 * 60 * 24);
|
||||
break;
|
||||
}
|
||||
case 'HH': {
|
||||
diff = duration / (1000 * 60 * 60);
|
||||
break;
|
||||
}
|
||||
case 'mm': {
|
||||
diff = duration / (1000 * 60);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Math.round(diff);
|
||||
};
|
||||
/**
|
||||
* @Title: 开始日期
|
||||
* @param last
|
||||
* @param type
|
||||
* @param formater
|
||||
* @returns
|
||||
*/
|
||||
export const startDate = (num, type = 'month', formater = 'YYYY-MM-DD HH:mm:ss') => {
|
||||
if (num === 'now') return dayjs().format(formater);
|
||||
if (typeof num === 'string') return dayjs(num).startOf(type).format(formater);
|
||||
return num === 0 ? dayjs().startOf(type).format(formater) : dayjs().subtract(num, type).startOf(type).format(formater);
|
||||
};
|
||||
/**
|
||||
* @Title: 结束日期
|
||||
* @param num
|
||||
* @param type
|
||||
* @param formater
|
||||
* @returns
|
||||
*/
|
||||
export const endDate = (num = 0, type = 'month', formater = 'YYYY-MM-DD HH:mm:ss') => {
|
||||
if (num === 'now') return dayjs().format(formater);
|
||||
if (typeof num === 'string') return dayjs(num).endOf(type).format(formater);
|
||||
return num === 0 ? dayjs().endOf(type).format(formater) : dayjs().subtract(num, type).endOf(type).format(formater);
|
||||
};
|
||||
|
||||
/**
|
||||
* @Title: 生成随机数
|
||||
* @param len
|
||||
* @returns
|
||||
*/
|
||||
export const randomNumber = (len) => {
|
||||
let randomlen = len ? len : 10;
|
||||
const chars = '0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < randomlen; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
};
|
56
src/utils/permission.js
Normal file
56
src/utils/permission.js
Normal file
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @Description: 路由权限
|
||||
* @Author: zenghua.wang
|
||||
* @Date: 2022-01-26 22:04:31
|
||||
* @LastEditors: zenghua.wang
|
||||
* @LastEditTime: 2024-02-26 13:54:43
|
||||
*/
|
||||
import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper';
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
import router from '@/router';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { usePermissionStore } from '@/store/modules/permission';
|
||||
|
||||
NProgress.configure({ showSpinner: false });
|
||||
|
||||
const { VITE_APP_TITLE } = import.meta.env;
|
||||
const whiteList = [];
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
NProgress.start();
|
||||
if (typeof to.meta.title === 'string') {
|
||||
document.title = `${VITE_APP_TITLE} | ` + to.meta.title;
|
||||
}
|
||||
|
||||
const userStore = useUserStore();
|
||||
const hasToken = true; //userStore.hasToken();
|
||||
if (hasToken) {
|
||||
if (to.path === '/login') {
|
||||
next({ path: '/' });
|
||||
} else {
|
||||
try {
|
||||
const PermissionStore = usePermissionStore();
|
||||
if (!PermissionStore.routes.length) {
|
||||
const accessRoutes = await PermissionStore.generateRoutes(userStore.roles);
|
||||
accessRoutes.forEach((item) => router.addRoute(item));
|
||||
return next({ ...to, replace: true });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
} catch (error) {
|
||||
next(`/login?redirect=${to.path}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NProgress.done();
|
||||
if (whiteList.indexOf(to.path) !== -1) {
|
||||
next();
|
||||
} else {
|
||||
next(`/login?redirect=${to.path}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
router.afterEach(() => {
|
||||
NProgress.done();
|
||||
});
|
111
src/utils/router.js
Normal file
111
src/utils/router.js
Normal file
@ -0,0 +1,111 @@
|
||||
/**
|
||||
* @Description: 路由方法
|
||||
* @Author: zenghua.wang
|
||||
* @Date: 2022-01-26 21:55:58
|
||||
* @LastEditors: zenghua.wang
|
||||
* @LastEditTime: 2024-04-13 21:51:35
|
||||
*/
|
||||
import path from 'path-browserify';
|
||||
import Layout from '@/layouts/index.vue';
|
||||
import Views from '@/layouts/Views.vue';
|
||||
import { isEmpty } from './index';
|
||||
|
||||
const modules = import.meta.glob('../views/**/**.vue');
|
||||
|
||||
/**
|
||||
* 创建路由菜单
|
||||
* @param {*} menus
|
||||
*/
|
||||
export function createAsyncRoutes(menus = [], isLayout = true) {
|
||||
if (isEmpty(menus)) return menus;
|
||||
const res = [];
|
||||
menus.forEach((menu) => {
|
||||
const tmp = {
|
||||
id: menu.id,
|
||||
parentId: menu.parentId,
|
||||
path: menu.path,
|
||||
component: isEmpty(menu.component)
|
||||
? isLayout
|
||||
? Layout
|
||||
: Views
|
||||
: modules[/* @vite-ignore */ `../views/${menu.component.replace('/views/', '')}`],
|
||||
redirect: menu.redirect,
|
||||
name: menu.name,
|
||||
meta: {
|
||||
title: menu.title,
|
||||
icon: menu?.icon || 'icon-demo',
|
||||
keepAlive: menu.keepAlive,
|
||||
},
|
||||
children: menu.children,
|
||||
};
|
||||
if (tmp.children) {
|
||||
tmp.children = createAsyncRoutes(tmp.children, false);
|
||||
}
|
||||
res.push(tmp);
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过递归过滤异步路由表
|
||||
* @param routes asyncRoutes
|
||||
* @param roles
|
||||
*/
|
||||
export function filterAsyncRoutes(routes, roles) {
|
||||
const res = [];
|
||||
routes.forEach((route) => {
|
||||
const tmp = { ...route };
|
||||
if (hasPermission(roles, tmp)) {
|
||||
if (tmp.children) {
|
||||
tmp.children = filterAsyncRoutes(tmp.children, roles);
|
||||
}
|
||||
res.push(tmp);
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 meta.role 来确定当前用户是否具有权限
|
||||
* @param roles
|
||||
* @param route
|
||||
*/
|
||||
export function hasPermission(roles, route) {
|
||||
if (route.meta && route.meta.roles) {
|
||||
return roles.some((role) => route.meta.roles.includes(role));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用递归,过滤需要缓存的路由
|
||||
* @param {*} routers
|
||||
* @returns
|
||||
*/
|
||||
export function filterKeepAlive(routers) {
|
||||
const cacheRouter = [];
|
||||
const loop = (routers) => {
|
||||
routers.forEach((item) => {
|
||||
if (item.meta?.keepAlive && item.name) {
|
||||
cacheRouter.push(item.name);
|
||||
}
|
||||
if (item.children && item.children.length) {
|
||||
loop(item.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
loop(routers);
|
||||
return cacheRouter;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} routers
|
||||
* @param {*} pathUrl
|
||||
*/
|
||||
export function handleRoutes(routers, pathUrl = '') {
|
||||
routers.forEach((item) => {
|
||||
item.path = path.resolve(pathUrl, item.path);
|
||||
});
|
||||
}
|
184
src/utils/validate.js
Normal file
184
src/utils/validate.js
Normal file
@ -0,0 +1,184 @@
|
||||
/**
|
||||
* @Description: 验证
|
||||
* @Author: zenghua.wang
|
||||
* @Date: 2022-01-25 21:08:52
|
||||
* @LastEditors: zenghua.wang
|
||||
* @LastEditTime: 2024-01-26 22:22:58
|
||||
*/
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isExternal(path) {
|
||||
return /^(https?:|mailto:|tel:)/.test(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validUsername(str) {
|
||||
const valid_map = ['admin', 'editor'];
|
||||
return valid_map.indexOf(str.trim()) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validURL(url) {
|
||||
const reg =
|
||||
/^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/;
|
||||
return reg.test(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validLowerCase(str) {
|
||||
const reg = /^[a-z]+$/;
|
||||
return reg.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validUpperCase(str) {
|
||||
const reg = /^[A-Z]+$/;
|
||||
return reg.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validAlphabets(str) {
|
||||
const reg = /^[A-Za-z]+$/;
|
||||
return reg.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} email
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validEmail(email) {
|
||||
const reg =
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return reg.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isString(str) {
|
||||
if (typeof str === 'string' || str instanceof String) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} arg
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isArray(arg) {
|
||||
if (typeof Array.isArray === 'undefined') {
|
||||
return Object.prototype.toString.call(arg) === '[object Array]';
|
||||
}
|
||||
return Array.isArray(arg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号码
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true: 手机号码正确
|
||||
*/
|
||||
export function verifyPhone(val) {
|
||||
// false: 手机号码不正确
|
||||
if (!/^((12[0-9])|(13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0|1,5-9]))\d{8}$/.test(val)) return false;
|
||||
// true: 手机号码正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配文字变色(搜索时)
|
||||
* @param val 当前值字符串
|
||||
* @param text 要处理的字符串值
|
||||
* @param color 搜索到时字体高亮颜色
|
||||
* @returns 返回处理后的字符串
|
||||
*/
|
||||
export function verifyTextColor(val, text = '', color = 'red') {
|
||||
// 返回内容,添加颜色
|
||||
const v = text.replace(new RegExp(val, 'gi'), `<span style='color: ${color}'>${val}</span>`);
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 身份证号, 支持1/2代(15位/18位数字)
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true: 身份证正确
|
||||
*/
|
||||
export function verifyIdCard(val) {
|
||||
const regx = /(^\d{8}(0\d|10|11|12)([0-2]\d|30|31)\d{3}$)|(^\d{6}(18|19|20)\d{2}(0\d|10|11|12)([0-2]\d|30|31)\d{3}(\d|X|x)$)/;
|
||||
return regx.test(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* 网址
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true: 网址正确
|
||||
*/
|
||||
export function verifyWebsite(val) {
|
||||
const regx = /^((https?|ftp):\/\/)?([\da-z.-]+)\.([a-z.]{2,6})(\/\w\.-]*)*\/?/;
|
||||
return regx.test(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否html标签
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true: 是否html标签
|
||||
*/
|
||||
export function verifyHtml(val) {
|
||||
const regx = /<(.*)>.*<\/\1>|<(.*) \/>/;
|
||||
return regx.test(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true: 是否日期
|
||||
*/
|
||||
export function verifyDate(val) {
|
||||
const regx =
|
||||
/^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$/;
|
||||
return regx.test(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true: 邮箱是否正确
|
||||
*/
|
||||
export function verifyEmail(val) {
|
||||
const regx = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
||||
return regx.test(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证校验器函数封装
|
||||
* @param verifyPhone 验证函数
|
||||
* @param message 提示
|
||||
*/
|
||||
export function validatorMethod(verifyPhone, message) {
|
||||
return (rule, value, callback) => {
|
||||
if (!verifyPhone(value)) {
|
||||
callback(new Error(message));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
}
|
64
src/views/demo/rank.vue
Normal file
64
src/views/demo/rank.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="demo">
|
||||
<h2 class="demo-title">滚动排名列表</h2>
|
||||
<custom-rank-List :chart-config="options" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
const options = ref({
|
||||
attr: { w: 200, h: 200 },
|
||||
option: {
|
||||
// 数据
|
||||
dataset: [
|
||||
{ name: '荣成', value: 26700 },
|
||||
{ name: '河南', value: 20700 },
|
||||
{ name: '河北', value: 18700 },
|
||||
{ name: '徐州', value: 17800 },
|
||||
{ name: '漯河', value: 16756 },
|
||||
{ name: '三门峡', value: 12343 },
|
||||
{ name: '郑州', value: 9822 },
|
||||
{ name: '周口', value: 8912 },
|
||||
{ name: '濮阳', value: 6834 },
|
||||
{ name: '信阳', value: 5875 },
|
||||
{ name: '新乡', value: 3832 },
|
||||
{ name: '大同', value: 1811 },
|
||||
],
|
||||
// 表行数
|
||||
rowNum: 5,
|
||||
// 轮播时间
|
||||
waitTime: 2,
|
||||
// 数值单位
|
||||
unit: '',
|
||||
// 自动排序
|
||||
sort: true,
|
||||
color: '#1370fb',
|
||||
textColor: '#CDD2F8FF',
|
||||
borderColor: '#1370fb80',
|
||||
carousel: 'single',
|
||||
//序号字体大小
|
||||
indexFontSize: 12,
|
||||
//左侧数据字体大小
|
||||
leftFontSize: 12,
|
||||
//右侧数据字体大小
|
||||
rightFontSize: 12,
|
||||
// 格式化
|
||||
valueFormatter: (item) => {
|
||||
return item.value;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.demo {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
margin: 30px auto 0;
|
||||
background-color: #fff;
|
||||
|
||||
&-title {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
50
src/views/demo/scrollBoard.vue
Normal file
50
src/views/demo/scrollBoard.vue
Normal file
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="demo">
|
||||
<h2 class="demo-title">轮播列表</h2>
|
||||
<custom-scroll-board :chart-config="boardOption" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
const boardOption = ref({
|
||||
attr: { w: 200, h: 200 },
|
||||
option: {
|
||||
header: ['列1', '列2', '列3'],
|
||||
dataset: [
|
||||
['行1列1', '行1列2', '行1列3'],
|
||||
['行2列1', '行2列2', '行2列3'],
|
||||
['行3列1', '行3列2', '行3列3'],
|
||||
['行4列1', '行4列2', '行4列3'],
|
||||
['行5列1', '行5列2', '行5列3'],
|
||||
['行6列1', '行6列2', '行6列3'],
|
||||
['行7列1', '行7列2', '行7列3'],
|
||||
['行8列1', '行8列2', '行8列3'],
|
||||
['行9列1', '行9列2', '行9列3'],
|
||||
['行10列1', '行10列2', '行10列3'],
|
||||
],
|
||||
index: true,
|
||||
columnWidth: [30, 100, 100],
|
||||
align: ['center', 'right', 'right', 'right'],
|
||||
rowNum: 5,
|
||||
waitTime: 2,
|
||||
headerHeight: 40,
|
||||
carousel: 'single',
|
||||
headerBGC: '#00BAFF',
|
||||
oddRowBGC: '#003B51',
|
||||
evenRowBGC: '#0A2732',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.demo {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
margin: 30px auto 0;
|
||||
background-color: #fff;
|
||||
|
||||
&-title {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
3
src/views/home/index.vue
Normal file
3
src/views/home/index.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>开发中</div>
|
||||
</template>
|
110
vite.config.js
Normal file
110
vite.config.js
Normal file
@ -0,0 +1,110 @@
|
||||
/*
|
||||
* @Descripttion:
|
||||
* @Author: zenghua.wang
|
||||
* @Date: 2022-09-18 21:24:29
|
||||
* @LastEditors: zenghua.wang
|
||||
* @LastEditTime: 2025-04-18 15:41:11
|
||||
*/
|
||||
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
// import qiankun from 'vite-plugin-qiankun';
|
||||
import eslintPlugin from 'vite-plugin-eslint';
|
||||
import vueSetupExtend from 'vite-plugin-vue-setup-extend';
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
||||
import compression from 'vite-plugin-compression';
|
||||
import { viteMockServe } from 'vite-plugin-mock';
|
||||
import AutoImport from 'unplugin-auto-import/vite';
|
||||
import Components from 'unplugin-vue-components/vite';
|
||||
import postcssImport from 'postcss-import';
|
||||
import autoprefixer from 'autoprefixer';
|
||||
import { resolve } from 'path';
|
||||
|
||||
const useDevMode = true;
|
||||
|
||||
export default defineConfig(({ command, mode }) => {
|
||||
const { VITE_PORT, VITE_APP_NAME, VITE_APP_BASE_API, VITE_APP_BASE_URL } = loadEnv(mode, process.cwd());
|
||||
const config = {
|
||||
base: '/',
|
||||
build: {
|
||||
target: 'ESNext',
|
||||
outDir: 'dist',
|
||||
minify: 'terser',
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: VITE_PORT,
|
||||
open: true,
|
||||
https: false,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
proxy: {
|
||||
[VITE_APP_BASE_API]: {
|
||||
target: VITE_APP_BASE_URL,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/apis/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
},
|
||||
extensions: ['.js', '.vue', '.json', '.ts'],
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: '@import "@/styles/global.scss";',
|
||||
api: 'modern-compiler',
|
||||
},
|
||||
},
|
||||
postcss: {
|
||||
plugins: [
|
||||
postcssImport,
|
||||
autoprefixer({
|
||||
overrideBrowserslist: ['> 1%', 'last 2 versions'],
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
// qiankun(VITE_APP_NAME, { useDevMode }),
|
||||
vueSetupExtend(),
|
||||
eslintPlugin({
|
||||
include: ['src/**/*.ts', 'src/**/*.vue', 'src/*.ts', 'src/*.vue'],
|
||||
}),
|
||||
Components({
|
||||
dirs: ['src/components'],
|
||||
extensions: ['vue', 'js', 'jsx', 'ts', 'tsx'],
|
||||
resolvers: [],
|
||||
}),
|
||||
compression(),
|
||||
AutoImport({
|
||||
include: [/\.[tj]s?$/, /\.vue$/],
|
||||
imports: ['vue', 'vue-router'],
|
||||
}),
|
||||
createSvgIconsPlugin({
|
||||
iconDirs: [resolve(process.cwd(), 'src/assets/svgs')],
|
||||
symbolId: 'icon-[name]',
|
||||
}),
|
||||
viteMockServe({
|
||||
mockPath: 'src/mock',
|
||||
watchFiles: true,
|
||||
localEnabled: command === 'dev',
|
||||
prodEnabled: false,
|
||||
}),
|
||||
],
|
||||
};
|
||||
if (mode === 'production') {
|
||||
config.build.terserOptions = {
|
||||
compress: {
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return config;
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user