@ -0,0 +1,12 @@ |
||||
root = true |
||||
[*.{js,ts,vue}] |
||||
charset = utf-8 # 设置文件字符集为 utf-8 |
||||
end_of_line = lf # 控制换行类型(lf | cr | crlf) |
||||
insert_final_newline = true # 始终在文件末尾插入一个新行 |
||||
indent_style = space # 缩进风格(tab | space) |
||||
indent_size = 2 # 缩进大小 |
||||
max_line_length = 120 # 最大行长度 |
||||
|
||||
[*.md] # 仅 md 文件适用以下规则 |
||||
max_line_length = off # 关闭最大行长度限制 |
||||
trim_trailing_whitespace = false # 关闭末尾空格修剪 |
@ -0,0 +1,20 @@ |
||||
# 标题 |
||||
VITE_APP_TITLE=刷题后台 |
||||
|
||||
# 开发环境 |
||||
VITE_NODE_ENV=development |
||||
|
||||
# 项目本地运行端口号 |
||||
VITE_PORT=8000 |
||||
|
||||
# open 运行 npm run dev 时自动打开浏览器 |
||||
VITE_OPEN=true |
||||
|
||||
# 租户开关 |
||||
VITE_APP_TENANT_ENABLE=false |
||||
|
||||
# 验证码的开关 |
||||
VITE_APP_CAPTCHA_ENABLE=true |
||||
|
||||
# 百度统计 |
||||
VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc |
@ -0,0 +1,24 @@ |
||||
# 本地开发环境 |
||||
VITE_NODE_ENV=development |
||||
|
||||
VITE_DEV=true |
||||
|
||||
# 请求路径 |
||||
# VITE_BASE_URL='http://localhost:48080' |
||||
|
||||
VITE_BASE_URL='http://47.98.161.246:48080' |
||||
# VITE_BASE_URL='http://114.55.169.15:48080' |
||||
# VITE_BASE_URL='http://114.215.207.150:48080' |
||||
|
||||
# 上传路径 |
||||
VITE_UPLOAD_URL='http://47.98.161.246:48080/admin-api/system/file/upload' |
||||
# VITE_UPLOAD_URL='http://114.55.169.15:48080/admin-api/system/file/upload' |
||||
|
||||
# 接口前缀 |
||||
VITE_API_BASEPATH=/tiku-api |
||||
|
||||
# 接口地址 |
||||
VITE_API_URL=/admin-api |
||||
|
||||
# 打包路径 |
||||
VITE_BASE_PATH=/tiku/ |
@ -0,0 +1,31 @@ |
||||
# 开发环境 |
||||
VITE_NODE_ENV=production |
||||
|
||||
VITE_DEV=false |
||||
|
||||
# 请求路径 |
||||
VITE_BASE_URL='http://localhost:48080' |
||||
|
||||
# 上传路径 |
||||
VITE_UPLOAD_URL='http://47.98.161.246:48080/admin-api/system/file/upload' |
||||
|
||||
# 接口前缀 |
||||
VITE_API_BASEPATH=/tiku-api |
||||
|
||||
# 接口地址 |
||||
VITE_API_URL=/admin-api |
||||
|
||||
# 打包路径 |
||||
VITE_BASE_PATH=/tiku/ |
||||
|
||||
# 是否删除debugger |
||||
VITE_DROP_DEBUGGER=false |
||||
|
||||
# 是否删除console.log |
||||
VITE_DROP_CONSOLE=false |
||||
|
||||
# 是否sourcemap |
||||
VITE_SOURCEMAP=true |
||||
|
||||
# 输出路径 |
||||
VITE_OUT_DIR=dist-dev |
@ -0,0 +1,34 @@ |
||||
# 本地开发环境 |
||||
VITE_NODE_ENV=development |
||||
|
||||
VITE_DEV=true |
||||
|
||||
# 请求路径 |
||||
VITE_BASE_URL='http://47.98.161.246:48080' |
||||
|
||||
# 上传路径 |
||||
VITE_UPLOAD_URL='http://47.98.161.246:48080/admin-api/system/file/upload' |
||||
|
||||
# 接口前缀 |
||||
VITE_API_BASEPATH=/tiku-api |
||||
|
||||
# 接口地址 |
||||
VITE_API_URL=/admin-api |
||||
|
||||
# 打包路径 |
||||
VITE_BASE_PATH=/tiku/ |
||||
|
||||
# 项目本地运行端口号, 与.vscode/launch.json配合 |
||||
VITE_PORT=80 |
||||
|
||||
# 是否删除debugger |
||||
VITE_DROP_DEBUGGER=false |
||||
|
||||
# 是否删除console.log |
||||
VITE_DROP_CONSOLE=false |
||||
|
||||
# 是否sourcemap |
||||
VITE_SOURCEMAP=true |
||||
|
||||
# 验证码的开关 |
||||
VITE_APP_CAPTCHA_ENABLE=false |
@ -0,0 +1,31 @@ |
||||
# 生产环境 |
||||
VITE_NODE_ENV=production |
||||
|
||||
VITE_DEV=false |
||||
|
||||
# 请求路径 |
||||
VITE_BASE_URL='/oa-api' |
||||
|
||||
# 上传路径 |
||||
VITE_UPLOAD_URL='/oa-api/admin-api/system/file/upload' |
||||
|
||||
# 接口前缀 |
||||
VITE_API_BASEPATH=/tiku-api |
||||
|
||||
# 接口地址 |
||||
VITE_API_URL=/admin-api |
||||
|
||||
# 是否删除debugger |
||||
VITE_DROP_DEBUGGER=true |
||||
|
||||
# 是否删除console.log |
||||
VITE_DROP_CONSOLE=true |
||||
|
||||
# 是否sourcemap |
||||
VITE_SOURCEMAP=false |
||||
|
||||
# 打包路径 |
||||
VITE_BASE_PATH=/tiku/ |
||||
|
||||
# 输出路径 |
||||
VITE_OUT_DIR=dist-pro |
@ -0,0 +1,31 @@ |
||||
# 生产环境 |
||||
VITE_NODE_ENV=production |
||||
|
||||
VITE_DEV=false |
||||
|
||||
# 请求路径 |
||||
VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn' |
||||
|
||||
# 上传路径 |
||||
VITE_UPLOAD_URL='http://47.98.161.246:48080/admin-api/system/file/upload' |
||||
|
||||
# 接口前缀 |
||||
VITE_API_BASEPATH=/tiku-api |
||||
|
||||
# 接口地址 |
||||
VITE_API_URL=/admin-api |
||||
|
||||
# 是否删除debugger |
||||
VITE_DROP_DEBUGGER=true |
||||
|
||||
# 是否删除console.log |
||||
VITE_DROP_CONSOLE=true |
||||
|
||||
# 是否sourcemap |
||||
VITE_SOURCEMAP=false |
||||
|
||||
# 打包路径 |
||||
VITE_BASE_PATH='/tiku/' |
||||
|
||||
# 输出路径 |
||||
VITE_OUT_DIR=dist-stage |
@ -0,0 +1,31 @@ |
||||
# 开发环境 |
||||
VITE_NODE_ENV=production |
||||
|
||||
VITE_DEV=false |
||||
|
||||
# 请求路径 |
||||
VITE_BASE_URL='http://localhost:48080' |
||||
|
||||
# 上传路径 |
||||
VITE_UPLOAD_URL='http://47.98.161.246:48080/admin-api/system/file/upload' |
||||
|
||||
# 接口前缀 |
||||
VITE_API_BASEPATH=/tiku-api |
||||
|
||||
# 接口地址 |
||||
VITE_API_URL=/admin-api |
||||
|
||||
# 是否删除debugger |
||||
VITE_DROP_DEBUGGER=true |
||||
|
||||
# 是否删除console.log |
||||
VITE_DROP_CONSOLE=true |
||||
|
||||
# 是否sourcemap |
||||
VITE_SOURCEMAP=false |
||||
|
||||
# 打包路径 |
||||
VITE_BASE_PATH=/tiku/ |
||||
|
||||
# 输出路径 |
||||
VITE_OUT_DIR=dist-dev |
@ -0,0 +1,31 @@ |
||||
# 测试环境 |
||||
VITE_NODE_ENV=production |
||||
|
||||
VITE_DEV=false |
||||
|
||||
# 请求路径 |
||||
VITE_BASE_URL='http://localhost:48080' |
||||
|
||||
# 上传路径 |
||||
VITE_UPLOAD_URL='http://47.98.161.246:48080/admin-api/system/file/upload' |
||||
|
||||
# 接口前缀 |
||||
VITE_API_BASEPATH=/tiku-api |
||||
|
||||
# 接口地址 |
||||
VITE_API_URL=/admin-api |
||||
|
||||
# 是否删除debugger |
||||
VITE_DROP_DEBUGGER=false |
||||
|
||||
# 是否删除console.log |
||||
VITE_DROP_CONSOLE=false |
||||
|
||||
# 是否sourcemap |
||||
VITE_SOURCEMAP=true |
||||
|
||||
# 打包路径 |
||||
VITE_BASE_PATH=/tiku/ |
||||
|
||||
# 输出路径 |
||||
VITE_OUT_DIR=dist-test |
@ -0,0 +1,8 @@ |
||||
/build/ |
||||
/config/ |
||||
/dist/ |
||||
/*.js |
||||
/test/unit/coverage/ |
||||
/node_modules/* |
||||
/dist* |
||||
/src/main.ts |
@ -0,0 +1,259 @@ |
||||
{ |
||||
"globals": { |
||||
"EffectScope": true, |
||||
"ElMessage": true, |
||||
"ElMessageBox": true, |
||||
"ElTag": true, |
||||
"asyncComputed": true, |
||||
"autoResetRef": true, |
||||
"computed": true, |
||||
"computedAsync": true, |
||||
"computedEager": true, |
||||
"computedInject": true, |
||||
"computedWithControl": true, |
||||
"controlledComputed": true, |
||||
"controlledRef": true, |
||||
"createApp": true, |
||||
"createEventHook": true, |
||||
"createGlobalState": true, |
||||
"createInjectionState": true, |
||||
"createReactiveFn": true, |
||||
"createSharedComposable": true, |
||||
"createUnrefFn": true, |
||||
"customRef": true, |
||||
"debouncedRef": true, |
||||
"debouncedWatch": true, |
||||
"defineAsyncComponent": true, |
||||
"defineComponent": true, |
||||
"eagerComputed": true, |
||||
"effectScope": true, |
||||
"extendRef": true, |
||||
"getCurrentInstance": true, |
||||
"getCurrentScope": true, |
||||
"h": true, |
||||
"ignorableWatch": true, |
||||
"inject": true, |
||||
"isDefined": true, |
||||
"isProxy": true, |
||||
"isReactive": true, |
||||
"isReadonly": true, |
||||
"isRef": true, |
||||
"makeDestructurable": true, |
||||
"markRaw": true, |
||||
"nextTick": true, |
||||
"onActivated": true, |
||||
"onBeforeMount": true, |
||||
"onBeforeUnmount": true, |
||||
"onBeforeUpdate": true, |
||||
"onClickOutside": true, |
||||
"onDeactivated": true, |
||||
"onErrorCaptured": true, |
||||
"onKeyStroke": true, |
||||
"onLongPress": true, |
||||
"onMounted": true, |
||||
"onRenderTracked": true, |
||||
"onRenderTriggered": true, |
||||
"onScopeDispose": true, |
||||
"onServerPrefetch": true, |
||||
"onStartTyping": true, |
||||
"onUnmounted": true, |
||||
"onUpdated": true, |
||||
"pausableWatch": true, |
||||
"provide": true, |
||||
"reactify": true, |
||||
"reactifyObject": true, |
||||
"reactive": true, |
||||
"reactiveComputed": true, |
||||
"reactiveOmit": true, |
||||
"reactivePick": true, |
||||
"readonly": true, |
||||
"ref": true, |
||||
"refAutoReset": true, |
||||
"refDebounced": true, |
||||
"refDefault": true, |
||||
"refThrottled": true, |
||||
"refWithControl": true, |
||||
"resolveComponent": true, |
||||
"resolveRef": true, |
||||
"resolveUnref": true, |
||||
"shallowReactive": true, |
||||
"shallowReadonly": true, |
||||
"shallowRef": true, |
||||
"syncRef": true, |
||||
"syncRefs": true, |
||||
"templateRef": true, |
||||
"throttledRef": true, |
||||
"throttledWatch": true, |
||||
"toRaw": true, |
||||
"toReactive": true, |
||||
"toRef": true, |
||||
"toRefs": true, |
||||
"triggerRef": true, |
||||
"tryOnBeforeMount": true, |
||||
"tryOnBeforeUnmount": true, |
||||
"tryOnMounted": true, |
||||
"tryOnScopeDispose": true, |
||||
"tryOnUnmounted": true, |
||||
"unref": true, |
||||
"unrefElement": true, |
||||
"until": true, |
||||
"useActiveElement": true, |
||||
"useArrayEvery": true, |
||||
"useArrayFilter": true, |
||||
"useArrayFind": true, |
||||
"useArrayFindIndex": true, |
||||
"useArrayJoin": true, |
||||
"useArrayMap": true, |
||||
"useArrayReduce": true, |
||||
"useArraySome": true, |
||||
"useAsyncQueue": true, |
||||
"useAsyncState": true, |
||||
"useAttrs": true, |
||||
"useBase64": true, |
||||
"useBattery": true, |
||||
"useBluetooth": true, |
||||
"useBreakpoints": true, |
||||
"useBroadcastChannel": true, |
||||
"useBrowserLocation": true, |
||||
"useCached": true, |
||||
"useClipboard": true, |
||||
"useColorMode": true, |
||||
"useConfirmDialog": true, |
||||
"useCounter": true, |
||||
"useCssModule": true, |
||||
"useCssVar": true, |
||||
"useCssVars": true, |
||||
"useCurrentElement": true, |
||||
"useCycleList": true, |
||||
"useDark": true, |
||||
"useDateFormat": true, |
||||
"useDebounce": true, |
||||
"useDebounceFn": true, |
||||
"useDebouncedRefHistory": true, |
||||
"useDeviceMotion": true, |
||||
"useDeviceOrientation": true, |
||||
"useDevicePixelRatio": true, |
||||
"useDevicesList": true, |
||||
"useDisplayMedia": true, |
||||
"useDocumentVisibility": true, |
||||
"useDraggable": true, |
||||
"useDropZone": true, |
||||
"useElementBounding": true, |
||||
"useElementByPoint": true, |
||||
"useElementHover": true, |
||||
"useElementSize": true, |
||||
"useElementVisibility": true, |
||||
"useEventBus": true, |
||||
"useEventListener": true, |
||||
"useEventSource": true, |
||||
"useEyeDropper": true, |
||||
"useFavicon": true, |
||||
"useFetch": true, |
||||
"useFileDialog": true, |
||||
"useFileSystemAccess": true, |
||||
"useFocus": true, |
||||
"useFocusWithin": true, |
||||
"useFps": true, |
||||
"useFullscreen": true, |
||||
"useGamepad": true, |
||||
"useGeolocation": true, |
||||
"useIdle": true, |
||||
"useImage": true, |
||||
"useInfiniteScroll": true, |
||||
"useIntersectionObserver": true, |
||||
"useInterval": true, |
||||
"useIntervalFn": true, |
||||
"useKeyModifier": true, |
||||
"useLastChanged": true, |
||||
"useLocalStorage": true, |
||||
"useMagicKeys": true, |
||||
"useManualRefHistory": true, |
||||
"useMediaControls": true, |
||||
"useMediaQuery": true, |
||||
"useMemoize": true, |
||||
"useMemory": true, |
||||
"useMounted": true, |
||||
"useMouse": true, |
||||
"useMouseInElement": true, |
||||
"useMousePressed": true, |
||||
"useMutationObserver": true, |
||||
"useNavigatorLanguage": true, |
||||
"useNetwork": true, |
||||
"useNow": true, |
||||
"useObjectUrl": true, |
||||
"useOffsetPagination": true, |
||||
"useOnline": true, |
||||
"usePageLeave": true, |
||||
"useParallax": true, |
||||
"usePermission": true, |
||||
"usePointer": true, |
||||
"usePointerSwipe": true, |
||||
"usePreferredColorScheme": true, |
||||
"usePreferredDark": true, |
||||
"usePreferredLanguages": true, |
||||
"useRafFn": true, |
||||
"useRefHistory": true, |
||||
"useResizeObserver": true, |
||||
"useRoute": true, |
||||
"useRouter": true, |
||||
"useScreenOrientation": true, |
||||
"useScreenSafeArea": true, |
||||
"useScriptTag": true, |
||||
"useScroll": true, |
||||
"useScrollLock": true, |
||||
"useSessionStorage": true, |
||||
"useShare": true, |
||||
"useSlots": true, |
||||
"useSpeechRecognition": true, |
||||
"useSpeechSynthesis": true, |
||||
"useStepper": true, |
||||
"useStorage": true, |
||||
"useStorageAsync": true, |
||||
"useStyleTag": true, |
||||
"useSupported": true, |
||||
"useSwipe": true, |
||||
"useTemplateRefsList": true, |
||||
"useTextDirection": true, |
||||
"useTextSelection": true, |
||||
"useTextareaAutosize": true, |
||||
"useThrottle": true, |
||||
"useThrottleFn": true, |
||||
"useThrottledRefHistory": true, |
||||
"useTimeAgo": true, |
||||
"useTimeout": true, |
||||
"useTimeoutFn": true, |
||||
"useTimeoutPoll": true, |
||||
"useTimestamp": true, |
||||
"useTitle": true, |
||||
"useToggle": true, |
||||
"useTransition": true, |
||||
"useUrlSearchParams": true, |
||||
"useUserMedia": true, |
||||
"useVModel": true, |
||||
"useVModels": true, |
||||
"useVibrate": true, |
||||
"useVirtualList": true, |
||||
"useWakeLock": true, |
||||
"useWebNotification": true, |
||||
"useWebSocket": true, |
||||
"useWebWorker": true, |
||||
"useWebWorkerFn": true, |
||||
"useWindowFocus": true, |
||||
"useWindowScroll": true, |
||||
"useWindowSize": true, |
||||
"watch": true, |
||||
"watchArray": true, |
||||
"watchAtMost": true, |
||||
"watchDebounced": true, |
||||
"watchEffect": true, |
||||
"watchIgnorable": true, |
||||
"watchOnce": true, |
||||
"watchPausable": true, |
||||
"watchPostEffect": true, |
||||
"watchSyncEffect": true, |
||||
"watchThrottled": true, |
||||
"watchTriggerable": true, |
||||
"watchWithFilter": true, |
||||
"whenever": true |
||||
} |
||||
} |
@ -0,0 +1,69 @@ |
||||
// @ts-check
|
||||
const { defineConfig } = require('eslint-define-config') |
||||
module.exports = defineConfig({ |
||||
root: true, |
||||
env: { |
||||
browser: true, |
||||
node: true, |
||||
es6: true |
||||
}, |
||||
parser: 'vue-eslint-parser', |
||||
parserOptions: { |
||||
parser: '@typescript-eslint/parser', |
||||
ecmaVersion: 2020, |
||||
sourceType: 'module', |
||||
jsxPragma: 'React', |
||||
ecmaFeatures: { |
||||
jsx: true |
||||
} |
||||
}, |
||||
extends: [ |
||||
'plugin:vue/vue3-recommended', |
||||
'plugin:@typescript-eslint/recommended', |
||||
'prettier', |
||||
'plugin:prettier/recommended', |
||||
'./.eslintrc-auto-import.json' |
||||
], |
||||
rules: { |
||||
'vue/script-setup-uses-vars': 'error', |
||||
'vue/no-reserved-component-names': 'off', |
||||
'@typescript-eslint/ban-ts-ignore': 'off', |
||||
'@typescript-eslint/explicit-function-return-type': 'off', |
||||
'@typescript-eslint/no-explicit-any': 'off', |
||||
'@typescript-eslint/no-var-requires': 'off', |
||||
'@typescript-eslint/no-empty-function': 'off', |
||||
'vue/custom-event-name-casing': 'off', |
||||
'no-use-before-define': 'off', |
||||
'@typescript-eslint/no-use-before-define': 'off', |
||||
'@typescript-eslint/ban-ts-comment': 'off', |
||||
'@typescript-eslint/ban-types': 'off', |
||||
'@typescript-eslint/no-non-null-assertion': 'off', |
||||
'@typescript-eslint/explicit-module-boundary-types': 'off', |
||||
'@typescript-eslint/no-unused-vars': 'error', |
||||
'no-unused-vars': 'error', |
||||
'space-before-function-paren': 'off', |
||||
|
||||
'vue/attributes-order': 'off', |
||||
'vue/one-component-per-file': 'off', |
||||
'vue/html-closing-bracket-newline': 'off', |
||||
'vue/max-attributes-per-line': 'off', |
||||
'vue/multiline-html-element-content-newline': 'off', |
||||
'vue/singleline-html-element-content-newline': 'off', |
||||
'vue/attribute-hyphenation': 'off', |
||||
'vue/require-default-prop': 'off', |
||||
'vue/require-explicit-emits': 'off', |
||||
'vue/html-self-closing': [ |
||||
'error', |
||||
{ |
||||
html: { |
||||
void: 'always', |
||||
normal: 'never', |
||||
component: 'always' |
||||
}, |
||||
svg: 'always', |
||||
math: 'always' |
||||
} |
||||
], |
||||
'vue/multi-word-component-names': 'off' |
||||
} |
||||
}) |
@ -1,11 +1,11 @@ |
||||
# ---> Vue |
||||
# gitignore template for Vue.js projects |
||||
# |
||||
# Recommended template: Node.gitignore |
||||
|
||||
# TODO: where does this rule come from? |
||||
docs/_book |
||||
|
||||
# TODO: where does this rule come from? |
||||
test/ |
||||
|
||||
node_modules |
||||
.DS_Store |
||||
dist |
||||
dist-ssr |
||||
*.local |
||||
/dist* |
||||
*-lock.* |
||||
pnpm-debug |
||||
auto-*.d.ts |
||||
.idea |
||||
.history |
||||
|
@ -0,0 +1,11 @@ |
||||
/node_modules/** |
||||
/dist/ |
||||
/dist* |
||||
/public/* |
||||
/docs/* |
||||
/vite.config.ts |
||||
/src/types/env.d.ts |
||||
/src/types/auto-components.d.ts |
||||
/src/types/auto-imports.d.ts |
||||
/docs/**/* |
||||
CHANGELOG |
@ -0,0 +1,6 @@ |
||||
/dist/* |
||||
/public/* |
||||
public/* |
||||
/dist* |
||||
/src/types/env.d.ts |
||||
/docs/**/* |
@ -0,0 +1,21 @@ |
||||
MIT License |
||||
|
||||
Copyright (c) 2021-present Archer |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
SOFTWARE. |
@ -0,0 +1,109 @@ |
||||
import { resolve } from 'path' |
||||
import Vue from '@vitejs/plugin-vue' |
||||
import VueJsx from '@vitejs/plugin-vue-jsx' |
||||
import WindiCSS from 'vite-plugin-windicss' |
||||
import progress from 'vite-plugin-progress' |
||||
import EslintPlugin from 'vite-plugin-eslint' |
||||
import PurgeIcons from 'vite-plugin-purge-icons' |
||||
import { ViteEjsPlugin } from 'vite-plugin-ejs' |
||||
// @ts-ignore
|
||||
import ElementPlus from 'unplugin-element-plus/vite' |
||||
import AutoImport from 'unplugin-auto-import/vite' |
||||
import Components from 'unplugin-vue-components/vite' |
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' |
||||
import viteCompression from 'vite-plugin-compression' |
||||
import topLevelAwait from 'vite-plugin-top-level-await' |
||||
import vueSetupExtend from 'vite-plugin-vue-setup-extend-plus' |
||||
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite' |
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' |
||||
|
||||
export function createVitePlugins() { |
||||
const root = process.cwd() |
||||
|
||||
// 路径查找
|
||||
function pathResolve(dir: string) { |
||||
return resolve(root, '.', dir) |
||||
} |
||||
|
||||
return [ |
||||
Vue(), |
||||
VueJsx(), |
||||
WindiCSS(), |
||||
progress(), |
||||
PurgeIcons(), |
||||
vueSetupExtend(), |
||||
ElementPlus({}), |
||||
AutoImport({ |
||||
include: [ |
||||
/\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
|
||||
/\.vue$/, |
||||
/\.vue\?vue/, // .vue
|
||||
/\.md$/ // .md
|
||||
], |
||||
imports: [ |
||||
'vue', |
||||
'vue-router', |
||||
// 可额外添加需要 autoImport 的组件
|
||||
{ |
||||
'@/hooks/web/useI18n': ['useI18n'], |
||||
'@/hooks/web/useMessage': ['useMessage'], |
||||
'@/hooks/web/useTable': ['useTable'], |
||||
'@/hooks/web/useCrudSchemas': ['useCrudSchemas'], |
||||
'@/utils/formRules': ['required'], |
||||
'@/utils/dict': ['DICT_TYPE'] |
||||
} |
||||
], |
||||
dts: 'src/types/auto-imports.d.ts', |
||||
resolvers: [ElementPlusResolver()], |
||||
eslintrc: { |
||||
enabled: false, // Default `false`
|
||||
filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
|
||||
globalsPropValue: true // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
|
||||
} |
||||
}), |
||||
Components({ |
||||
// 要搜索组件的目录的相对路径
|
||||
dirs: ['src/components'], |
||||
// 组件的有效文件扩展名
|
||||
extensions: ['vue', 'md'], |
||||
// 搜索子目录
|
||||
deep: true, |
||||
include: [/\.vue$/, /\.vue\?vue/], |
||||
// 生成自定义 `auto-components.d.ts` 全局声明
|
||||
dts: 'src/types/auto-components.d.ts', |
||||
// 自定义组件的解析器
|
||||
resolvers: [ElementPlusResolver()], |
||||
exclude: [/[\\/]node_modules[\\/]/] |
||||
}), |
||||
EslintPlugin({ |
||||
cache: false, |
||||
include: ['src/**/*.vue', 'src/**/*.ts', 'src/**/*.tsx'] // 检查的文件
|
||||
}), |
||||
VueI18nPlugin({ |
||||
runtimeOnly: true, |
||||
compositionOnly: true, |
||||
include: [resolve(__dirname, 'src/locales/**')] |
||||
}), |
||||
createSvgIconsPlugin({ |
||||
iconDirs: [pathResolve('src/assets/svgs')], |
||||
symbolId: 'icon-[dir]-[name]', |
||||
svgoOptions: true |
||||
}), |
||||
viteCompression({ |
||||
verbose: true, // 是否在控制台输出压缩结果
|
||||
disable: false, // 是否禁用
|
||||
threshold: 10240, // 体积大于 threshold 才会被压缩,单位 b
|
||||
algorithm: 'gzip', // 压缩算法,可选 [ 'gzip' , 'brotliCompress' ,'deflate' , 'deflateRaw']
|
||||
ext: '.gz', // 生成的压缩包后缀
|
||||
deleteOriginFile: false //压缩后是否删除源文件
|
||||
}), |
||||
ViteEjsPlugin(), |
||||
topLevelAwait({ |
||||
// https://juejin.cn/post/7152191742513512485
|
||||
// The export name of top-level await promise for each chunk module
|
||||
promiseExportName: '__tla', |
||||
// The function to generate import names of top-level await promise in each chunk module
|
||||
promiseImportName: (i) => `__tla_${i}` |
||||
}) |
||||
] |
||||
} |
@ -0,0 +1,111 @@ |
||||
const include = [ |
||||
'qs', |
||||
'url', |
||||
'vue', |
||||
'sass', |
||||
'mitt', |
||||
'axios', |
||||
'pinia', |
||||
'dayjs', |
||||
'qrcode', |
||||
'windicss', |
||||
'vue-router', |
||||
'vue-types', |
||||
'vue-i18n', |
||||
'xe-utils', |
||||
'crypto-js', |
||||
'cropperjs', |
||||
'lodash-es', |
||||
'nprogress', |
||||
'web-storage-cache', |
||||
'@iconify/iconify', |
||||
'@vueuse/core', |
||||
'@zxcvbn-ts/core', |
||||
'echarts/core', |
||||
'echarts/charts', |
||||
'echarts/components', |
||||
'echarts/renderers', |
||||
'echarts-wordcloud', |
||||
'@wangeditor/editor', |
||||
'@wangeditor/editor-for-vue', |
||||
'element-plus', |
||||
'element-plus/es', |
||||
'element-plus/es/locale/lang/zh-cn', |
||||
'element-plus/es/locale/lang/en', |
||||
'element-plus/es/components/backtop/style/css', |
||||
'element-plus/es/components/form/style/css', |
||||
'element-plus/es/components/radio-group/style/css', |
||||
'element-plus/es/components/radio/style/css', |
||||
'element-plus/es/components/checkbox/style/css', |
||||
'element-plus/es/components/checkbox-group/style/css', |
||||
'element-plus/es/components/switch/style/css', |
||||
'element-plus/es/components/time-picker/style/css', |
||||
'element-plus/es/components/date-picker/style/css', |
||||
'element-plus/es/components/descriptions/style/css', |
||||
'element-plus/es/components/descriptions-item/style/css', |
||||
'element-plus/es/components/link/style/css', |
||||
'element-plus/es/components/tooltip/style/css', |
||||
'element-plus/es/components/drawer/style/css', |
||||
'element-plus/es/components/dialog/style/css', |
||||
'element-plus/es/components/checkbox-button/style/css', |
||||
'element-plus/es/components/option-group/style/css', |
||||
'element-plus/es/components/radio-button/style/css', |
||||
'element-plus/es/components/cascader/style/css', |
||||
'element-plus/es/components/color-picker/style/css', |
||||
'element-plus/es/components/input-number/style/css', |
||||
'element-plus/es/components/rate/style/css', |
||||
'element-plus/es/components/select-v2/style/css', |
||||
'element-plus/es/components/tree-select/style/css', |
||||
'element-plus/es/components/slider/style/css', |
||||
'element-plus/es/components/time-select/style/css', |
||||
'element-plus/es/components/autocomplete/style/css', |
||||
'element-plus/es/components/image-viewer/style/css', |
||||
'element-plus/es/components/upload/style/css', |
||||
'element-plus/es/components/col/style/css', |
||||
'element-plus/es/components/form-item/style/css', |
||||
'element-plus/es/components/alert/style/css', |
||||
'element-plus/es/components/breadcrumb/style/css', |
||||
'element-plus/es/components/select/style/css', |
||||
'element-plus/es/components/input/style/css', |
||||
'element-plus/es/components/breadcrumb-item/style/css', |
||||
'element-plus/es/components/tag/style/css', |
||||
'element-plus/es/components/pagination/style/css', |
||||
'element-plus/es/components/table/style/css', |
||||
'element-plus/es/components/table-v2/style/css', |
||||
'element-plus/es/components/table-column/style/css', |
||||
'element-plus/es/components/card/style/css', |
||||
'element-plus/es/components/row/style/css', |
||||
'element-plus/es/components/button/style/css', |
||||
'element-plus/es/components/menu/style/css', |
||||
'element-plus/es/components/sub-menu/style/css', |
||||
'element-plus/es/components/menu-item/style/css', |
||||
'element-plus/es/components/option/style/css', |
||||
'element-plus/es/components/dropdown/style/css', |
||||
'element-plus/es/components/dropdown-menu/style/css', |
||||
'element-plus/es/components/dropdown-item/style/css', |
||||
'element-plus/es/components/skeleton/style/css', |
||||
'element-plus/es/components/skeleton/style/css', |
||||
'element-plus/es/components/backtop/style/css', |
||||
'element-plus/es/components/menu/style/css', |
||||
'element-plus/es/components/sub-menu/style/css', |
||||
'element-plus/es/components/menu-item/style/css', |
||||
'element-plus/es/components/dropdown/style/css', |
||||
'element-plus/es/components/tree/style/css', |
||||
'element-plus/es/components/dropdown-menu/style/css', |
||||
'element-plus/es/components/dropdown-item/style/css', |
||||
'element-plus/es/components/badge/style/css', |
||||
'element-plus/es/components/breadcrumb/style/css', |
||||
'element-plus/es/components/breadcrumb-item/style/css', |
||||
'element-plus/es/components/image/style/css', |
||||
'element-plus/es/components/collapse-transition/style/css', |
||||
'element-plus/es/components/timeline/style/css', |
||||
'element-plus/es/components/timeline-item/style/css', |
||||
'element-plus/es/components/collapse/style/css', |
||||
'element-plus/es/components/collapse-item/style/css', |
||||
'element-plus/es/components/button-group/style/css', |
||||
'element-plus/es/components/text/style/css' |
||||
] |
||||
|
||||
const exclude = ['@iconify/json'] |
||||
|
||||
export { include, exclude } |
@ -0,0 +1,143 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="en"> |
||||
<head> |
||||
<meta charset="UTF-8" /> |
||||
<link rel="icon" href="/favicon.ico" /> |
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||
<title>%VITE_APP_TITLE%</title> |
||||
</head> |
||||
<body> |
||||
<div id="app"> |
||||
<style> |
||||
.app-loading { |
||||
display: flex; |
||||
width: 100%; |
||||
height: 100%; |
||||
justify-content: center; |
||||
align-items: center; |
||||
flex-direction: column; |
||||
background: #f0f2f5; |
||||
} |
||||
|
||||
.app-loading .app-loading-wrap { |
||||
position: absolute; |
||||
top: 50%; |
||||
left: 50%; |
||||
display: flex; |
||||
-webkit-transform: translate3d(-50%, -50%, 0); |
||||
transform: translate3d(-50%, -50%, 0); |
||||
justify-content: center; |
||||
align-items: center; |
||||
flex-direction: column; |
||||
} |
||||
|
||||
.app-loading .app-loading-title { |
||||
margin-bottom: 30px; |
||||
font-size: 20px; |
||||
font-weight: bold; |
||||
text-align: center; |
||||
} |
||||
|
||||
.app-loading .app-loading-logo { |
||||
width: 100px; |
||||
margin: 0 auto 15px auto; |
||||
} |
||||
|
||||
.app-loading .app-loading-item { |
||||
position: relative; |
||||
display: inline-block; |
||||
width: 60px; |
||||
height: 60px; |
||||
vertical-align: middle; |
||||
border-radius: 50%; |
||||
} |
||||
|
||||
.app-loading .app-loading-outter { |
||||
position: absolute; |
||||
width: 100%; |
||||
height: 100%; |
||||
border: 4px solid #2d8cf0; |
||||
border-bottom: 0; |
||||
border-left-color: transparent; |
||||
border-radius: 50%; |
||||
animation: loader-outter 1s cubic-bezier(0.42, 0.61, 0.58, 0.41) infinite; |
||||
} |
||||
|
||||
.app-loading .app-loading-inner { |
||||
position: absolute; |
||||
top: calc(50% - 20px); |
||||
left: calc(50% - 20px); |
||||
width: 40px; |
||||
height: 40px; |
||||
border: 4px solid #87bdff; |
||||
border-right: 0; |
||||
border-top-color: transparent; |
||||
border-radius: 50%; |
||||
animation: loader-inner 1s cubic-bezier(0.42, 0.61, 0.58, 0.41) infinite; |
||||
} |
||||
|
||||
@-webkit-keyframes loader-outter { |
||||
0% { |
||||
-webkit-transform: rotate(0deg); |
||||
transform: rotate(0deg); |
||||
} |
||||
|
||||
100% { |
||||
-webkit-transform: rotate(360deg); |
||||
transform: rotate(360deg); |
||||
} |
||||
} |
||||
|
||||
@keyframes loader-outter { |
||||
0% { |
||||
-webkit-transform: rotate(0deg); |
||||
transform: rotate(0deg); |
||||
} |
||||
|
||||
100% { |
||||
-webkit-transform: rotate(360deg); |
||||
transform: rotate(360deg); |
||||
} |
||||
} |
||||
|
||||
@-webkit-keyframes loader-inner { |
||||
0% { |
||||
-webkit-transform: rotate(0deg); |
||||
transform: rotate(0deg); |
||||
} |
||||
|
||||
100% { |
||||
-webkit-transform: rotate(-360deg); |
||||
transform: rotate(-360deg); |
||||
} |
||||
} |
||||
|
||||
@keyframes loader-inner { |
||||
0% { |
||||
-webkit-transform: rotate(0deg); |
||||
transform: rotate(0deg); |
||||
} |
||||
|
||||
100% { |
||||
-webkit-transform: rotate(-360deg); |
||||
transform: rotate(-360deg); |
||||
} |
||||
} |
||||
</style> |
||||
<div class="app-loading"> |
||||
<div class="app-loading-wrap"> |
||||
<div class="app-loading-title"> |
||||
<img src="/logo.gif" class="app-loading-logo" alt="Logo" /> |
||||
<div class="app-loading-title">%VITE_APP_TITLE%</div> |
||||
</div> |
||||
<div class="app-loading-item"> |
||||
<div class="app-loading-outter"></div> |
||||
<div class="app-loading-inner"></div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<script type="module" src="/src/main.js"></script> |
||||
</body> |
||||
</html> |
@ -0,0 +1,142 @@ |
||||
{ |
||||
"name": "ss-tiku-manage", |
||||
"version": "1.0.1", |
||||
"description": "莳松题库管理系统", |
||||
"author": "ss", |
||||
"private": false, |
||||
"scripts": { |
||||
"i": "pnpm install", |
||||
"dev": "vite --mode base", |
||||
"dev-front": "vite --mode front", |
||||
"front": "vite --mode front", |
||||
"ts:check": "vue-tsc --noEmit", |
||||
"build:pro": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode pro", |
||||
"build:dev": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode dev", |
||||
"build:stage": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode stage", |
||||
"build:test": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode test", |
||||
"build:static": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode static", |
||||
"build:front": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode front", |
||||
"serve:pro": "vite preview --mode pro", |
||||
"serve:dev": "vite preview --mode dev", |
||||
"serve:test": "vite preview --mode test", |
||||
"preview": "pnpm build && vite preview", |
||||
"npm:check": "npx npm-check-updates", |
||||
"clean": "npx rimraf node_modules", |
||||
"clean:cache": "npx rimraf node_modules/.cache", |
||||
"lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src", |
||||
"lint:format": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,scss,vue,html,md}\"", |
||||
"lint:style": "stylelint --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/", |
||||
"lint:lint-staged": "lint-staged -c ", |
||||
"lint:pretty": "pretty-quick --staged" |
||||
}, |
||||
"dependencies": { |
||||
"@amap/amap-jsapi-loader": "^1.0.1", |
||||
"@element-plus/icons-vue": "^2.1.0", |
||||
"@form-create/designer": "^3.1.0", |
||||
"@form-create/element-ui": "^3.1.17", |
||||
"@iconify/iconify": "^3.1.0", |
||||
"@videojs-player/vue": "^1.0.0", |
||||
"@vueuse/core": "^10.1.2", |
||||
"@wangeditor/editor": "^5.1.23", |
||||
"@wangeditor/editor-for-vue": "^5.1.10", |
||||
"@zxcvbn-ts/core": "^3.0.1", |
||||
"animate.css": "^4.1.1", |
||||
"axios": "^1.4.0", |
||||
"benz-amr-recorder": "^1.1.5", |
||||
"bpmn-js-token-simulation": "^0.10.0", |
||||
"camunda-bpmn-moddle": "^7.0.1", |
||||
"cropperjs": "^1.5.13", |
||||
"crypto-js": "^4.1.1", |
||||
"dayjs": "^1.11.7", |
||||
"diagram-js": "^11.6.0", |
||||
"echarts": "^5.4.2", |
||||
"echarts-wordcloud": "^2.1.0", |
||||
"element-plus": "2.9.4", |
||||
"fast-xml-parser": "^4.2.2", |
||||
"highlight.js": "^11.8.0", |
||||
"intro.js": "^7.0.1", |
||||
"jsencrypt": "^3.3.2", |
||||
"lodash-es": "^4.17.21", |
||||
"min-dash": "^4.1.1", |
||||
"mitt": "^3.0.0", |
||||
"nprogress": "^0.2.0", |
||||
"pinia": "^2.1.3", |
||||
"qrcode": "^1.5.3", |
||||
"qs": "^6.11.2", |
||||
"steady-xml": "^0.1.0", |
||||
"url": "^0.11.0", |
||||
"video.js": "^8.3.0", |
||||
"vue": "3.3.4", |
||||
"vue-amap": "^0.5.10", |
||||
"vue-dompurify-html": "^5.0.1", |
||||
"vue-i18n": "9.2.2", |
||||
"vue-router": "^4.2.1", |
||||
"vue-types": "^5.0.3", |
||||
"vue3-tree-org": "^4.2.2", |
||||
"vuedraggable": "^4.1.0", |
||||
"web-storage-cache": "^1.1.1", |
||||
"xe-utils": "^3.5.7", |
||||
"xml-js": "^1.6.11" |
||||
}, |
||||
"devDependencies": { |
||||
"@commitlint/cli": "^17.6.3", |
||||
"@commitlint/config-conventional": "^17.6.3", |
||||
"@iconify/json": "^2.2.67", |
||||
"@intlify/unplugin-vue-i18n": "^0.10.0", |
||||
"@purge-icons/generated": "^0.9.0", |
||||
"@types/intro.js": "^5.1.1", |
||||
"@types/lodash-es": "^4.17.7", |
||||
"@types/node": "^18.16.0", |
||||
"@types/nprogress": "^0.2.0", |
||||
"@types/qrcode": "^1.5.0", |
||||
"@types/qs": "^6.9.7", |
||||
"@typescript-eslint/eslint-plugin": "^5.59.6", |
||||
"@typescript-eslint/parser": "^5.59.6", |
||||
"@vitejs/plugin-legacy": "^4.0.3", |
||||
"@vitejs/plugin-vue": "^4.2.3", |
||||
"@vitejs/plugin-vue-jsx": "^3.0.1", |
||||
"autoprefixer": "^10.4.14", |
||||
"bpmn-js": "^8.9.0", |
||||
"bpmn-js-properties-panel": "^0.46.0", |
||||
"consola": "^3.1.0", |
||||
"eslint": "^8.40.0", |
||||
"eslint-config-prettier": "^8.8.0", |
||||
"eslint-define-config": "^1.20.0", |
||||
"eslint-plugin-prettier": "^4.2.1", |
||||
"eslint-plugin-vue": "^9.13.0", |
||||
"lint-staged": "^13.2.2", |
||||
"postcss": "^8.4.23", |
||||
"postcss-html": "^1.5.0", |
||||
"postcss-scss": "^4.0.6", |
||||
"prettier": "^2.8.8", |
||||
"rimraf": "^5.0.1", |
||||
"rollup": "^3.22.0", |
||||
"sass": "^1.62.1", |
||||
"stylelint": "^15.6.2", |
||||
"stylelint-config-html": "^1.1.0", |
||||
"stylelint-config-recommended": "^12.0.0", |
||||
"stylelint-config-standard": "^33.0.0", |
||||
"stylelint-order": "^6.0.3", |
||||
"terser": "^5.17.4", |
||||
"typescript": "5.0.4", |
||||
"unplugin-auto-import": "^0.16.0", |
||||
"unplugin-element-plus": "^0.7.1", |
||||
"unplugin-vue-components": "^0.24.1", |
||||
"vite": "4.3.8", |
||||
"vite-plugin-compression": "^0.5.1", |
||||
"vite-plugin-ejs": "^1.6.4", |
||||
"vite-plugin-eslint": "^1.8.1", |
||||
"vite-plugin-progress": "^0.0.7", |
||||
"vite-plugin-purge-icons": "^0.9.2", |
||||
"vite-plugin-svg-icons": "^2.0.1", |
||||
"vite-plugin-top-level-await": "^1.3.0", |
||||
"vite-plugin-vue-setup-extend-plus": "^0.1.0", |
||||
"vite-plugin-windicss": "^1.9.0", |
||||
"vue-tsc": "^1.6.5", |
||||
"windicss": "^3.5.6" |
||||
}, |
||||
"engines": { |
||||
"node": ">=16.0.0" |
||||
}, |
||||
"license": "MIT" |
||||
} |
@ -0,0 +1,5 @@ |
||||
module.exports = { |
||||
plugins: { |
||||
autoprefixer: {} |
||||
} |
||||
} |
@ -0,0 +1,22 @@ |
||||
module.exports = { |
||||
printWidth: 100, // 每行代码长度(默认80)
|
||||
tabWidth: 2, // 每个tab相当于多少个空格(默认2)ab进行缩进(默认false)
|
||||
useTabs: false, // 是否使用tab
|
||||
semi: false, // 声明结尾使用分号(默认true)
|
||||
vueIndentScriptAndStyle: false, |
||||
singleQuote: true, // 使用单引号(默认false)
|
||||
quoteProps: 'as-needed', |
||||
bracketSpacing: true, // 对象字面量的大括号间使用空格(默认true)
|
||||
trailingComma: 'none', // 多行使用拖尾逗号(默认none)
|
||||
jsxSingleQuote: false, |
||||
// 箭头函数参数括号 默认avoid 可选 avoid| always
|
||||
// avoid 能省略括号的时候就省略 例如x => x
|
||||
// always 总是有括号
|
||||
arrowParens: 'always', |
||||
insertPragma: false, |
||||
requirePragma: false, |
||||
proseWrap: 'never', |
||||
htmlWhitespaceSensitivity: 'strict', |
||||
endOfLine: 'auto', |
||||
rangeStart: 0 |
||||
} |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 6.2 KiB |
@ -0,0 +1,54 @@ |
||||
<script lang="ts" name="APP" setup> |
||||
import { isDark } from '@/utils/is' |
||||
import { useAppStore } from '@/store/modules/app' |
||||
import { useDesign } from '@/hooks/web/useDesign' |
||||
import { CACHE_KEY } from '@/hooks/web/useCache' |
||||
import routerSearch from '@/components/RouterSearch/index.vue' |
||||
import cache from '@/plugins/cache' |
||||
|
||||
const { getPrefixCls } = useDesign() |
||||
const prefixCls = getPrefixCls('app') |
||||
const appStore = useAppStore() |
||||
const currentSize = computed(() => appStore.getCurrentSize) |
||||
const greyMode = computed(() => appStore.getGreyMode) |
||||
|
||||
// 根据浏览器当前主题设置系统主题色 |
||||
const setDefaultTheme = () => { |
||||
let isDarkTheme = cache.local.get(CACHE_KEY.IS_DARK) |
||||
if (isDarkTheme === null) { |
||||
isDarkTheme = isDark() |
||||
} |
||||
appStore.setIsDark(isDarkTheme) |
||||
} |
||||
setDefaultTheme() |
||||
</script> |
||||
<template> |
||||
<ConfigGlobal :size="currentSize"> |
||||
<RouterView :class="greyMode ? `${prefixCls}-grey-mode` : ''" /> |
||||
<routerSearch /> |
||||
</ConfigGlobal> |
||||
</template> |
||||
<style lang="scss"> |
||||
$prefix-cls: #{$namespace}-app; |
||||
|
||||
.size { |
||||
width: 100%; |
||||
height: 100%; |
||||
} |
||||
|
||||
html, |
||||
body { |
||||
padding: 0 !important; |
||||
margin: 0; |
||||
overflow: hidden; |
||||
@extend .size; |
||||
|
||||
#app { |
||||
@extend .size; |
||||
} |
||||
} |
||||
|
||||
.#{$prefix-cls}-grey-mode { |
||||
filter: grayscale(100%); |
||||
} |
||||
</style> |
@ -0,0 +1,36 @@ |
||||
import request from '@/config/axios' |
||||
|
||||
// 创建
|
||||
export const createCustomer = (data) => { |
||||
return request.post({ url: '/admin-api/tiku/customer/create', data, isSubmitForm: true }) |
||||
} |
||||
|
||||
// 分页
|
||||
export const getCustomerSimpleList = (params) => { |
||||
return request.get({ url: '/admin-api/tiku/customer/list-all-simple', params }) |
||||
} |
||||
|
||||
// 修改
|
||||
export const updateCustomer = (data) => { |
||||
return request.put({ url: '/admin-api/tiku/customer/update', data }) |
||||
} |
||||
|
||||
// 分页
|
||||
export const getCustomerPage = (params) => { |
||||
return request.get({ url: '/admin-api/tiku/customer/page', params }) |
||||
} |
||||
|
||||
// 详情
|
||||
export const getCustomerDetail = (id) => { |
||||
return request.get({ url: '/admin-api/tiku/customer/get', params: { id } }) |
||||
} |
||||
|
||||
// 删除
|
||||
export const deleteCustomer = (id) => { |
||||
return request.delete({ url: '/admin-api/tiku/customer/delete', params: { id } }) |
||||
} |
||||
|
||||
// 获取钉钉id
|
||||
export const getDingUserId = (params) => { |
||||
return request.get({ url: '/admin-api/tiku/customer/getDingTalkUserIdByMobile', params }) |
||||
} |
@ -0,0 +1,48 @@ |
||||
import request from '@/config/axios' |
||||
|
||||
export interface ConfigVO { |
||||
id: number | undefined |
||||
category: string |
||||
name: string |
||||
key: string |
||||
value: string |
||||
type: number |
||||
visible: boolean |
||||
remark: string |
||||
createTime: Date |
||||
} |
||||
|
||||
// 查询参数列表
|
||||
export const getConfigPage = (params: PageParam) => { |
||||
return request.get({ url: '/admin-api/infra/config/page', params }) |
||||
} |
||||
|
||||
// 查询参数详情
|
||||
export const getConfig = (id: number) => { |
||||
return request.get({ url: '/admin-api/infra/config/get?id=' + id }) |
||||
} |
||||
|
||||
// 根据参数键名查询参数值
|
||||
export const getConfigKey = (configKey: string) => { |
||||
return request.get({ url: '/admin-api/infra/config/get-value-by-key?key=' + configKey }) |
||||
} |
||||
|
||||
// 新增参数
|
||||
export const createConfig = (data: ConfigVO) => { |
||||
return request.post({ url: '/admin-api/infra/config/create', data }) |
||||
} |
||||
|
||||
// 修改参数
|
||||
export const updateConfig = (data: ConfigVO) => { |
||||
return request.put({ url: '/admin-api/infra/config/update', data }) |
||||
} |
||||
|
||||
// 删除参数
|
||||
export const deleteConfig = (id: number) => { |
||||
return request.delete({ url: '/admin-api/infra/config/delete?id=' + id }) |
||||
} |
||||
|
||||
// 导出参数
|
||||
export const exportConfig = (params) => { |
||||
return request.download({ url: '/admin-api/infra/config/export', params }) |
||||
} |
@ -0,0 +1,45 @@ |
||||
import request from '@/config/axios' |
||||
|
||||
export interface FilePageReqVO extends PageParam { |
||||
path?: string |
||||
type?: string |
||||
createTime?: Date[] |
||||
} |
||||
|
||||
// 文件预签名地址 Response VO
|
||||
export interface FilePresignedUrlRespVO { |
||||
// 文件配置编号
|
||||
configId: number |
||||
// 文件上传 URL
|
||||
uploadUrl: string |
||||
// 文件 URL
|
||||
url: string |
||||
} |
||||
|
||||
// 查询文件列表
|
||||
export const getFilePage = (params: FilePageReqVO) => { |
||||
return request.get({ url: '/infra/file/page', params }) |
||||
} |
||||
|
||||
// 删除文件
|
||||
export const deleteFile = (id: number) => { |
||||
return request.delete({ url: '/infra/file/delete?id=' + id }) |
||||
} |
||||
|
||||
// 获取文件预签名地址
|
||||
export const getFilePresignedUrl = (path: string) => { |
||||
return request.get<FilePresignedUrlRespVO>({ |
||||
url: '/infra/file/presigned-url', |
||||
params: { path } |
||||
}) |
||||
} |
||||
|
||||
// 创建文件
|
||||
export const createFile = (data: any) => { |
||||
return request.post({ url: '/infra/file/create', data }) |
||||
} |
||||
|
||||
// 上传文件
|
||||
export const updateFile = (data: any) => { |
||||
return request.upload({ url: '/admin-api/system/file/upload', data }) |
||||
} |
@ -0,0 +1,73 @@ |
||||
import request from '@/config/axios' |
||||
import { getRefreshToken } from '@/utils/auth' |
||||
import type { UserLoginVO } from './types' |
||||
|
||||
export interface SmsCodeVO { |
||||
mobile: string |
||||
scene: number |
||||
} |
||||
|
||||
export interface SmsLoginVO { |
||||
mobile: string |
||||
code: string |
||||
} |
||||
|
||||
// 登录
|
||||
export const login = (data: UserLoginVO) => { |
||||
return request.post({ url: '/admin-api/system/auth/login', data }) |
||||
} |
||||
|
||||
// 刷新访问令牌
|
||||
export const refreshToken = () => { |
||||
return request.post({ |
||||
url: '/admin-api/system/auth/refresh-token?refreshToken=' + getRefreshToken() |
||||
}) |
||||
} |
||||
|
||||
// 使用租户名,获得租户编号
|
||||
export const getTenantIdByName = (name: string) => { |
||||
return request.get({ url: '/admin-api/system/tenant/get-id-by-name?name=' + name }) |
||||
} |
||||
|
||||
// 登出
|
||||
export const loginOut = () => { |
||||
return request.post({ url: '/admin-api/system/auth/logout' }) |
||||
} |
||||
|
||||
// 获取用户权限信息
|
||||
export const getInfo = (params) => { |
||||
return request.get({ url: '/admin-api/system/auth/get-permission-info', params }) |
||||
} |
||||
|
||||
//获取登录验证码
|
||||
export const sendSmsCode = (data: SmsCodeVO) => { |
||||
return request.post({ url: '/admin-api/system/auth/send-sms-code', data }) |
||||
} |
||||
|
||||
// 短信验证码登录
|
||||
export const smsLogin = (data: SmsLoginVO) => { |
||||
return request.post({ url: '/admin-api/system/auth/sms-login', data }) |
||||
} |
||||
|
||||
// 社交授权的跳转
|
||||
export const socialAuthRedirect = (type: number, redirectUri: string) => { |
||||
return request.get({ |
||||
url: '/admin-api/system/auth/social-auth-redirect?type=' + type + '&redirectUri=' + redirectUri |
||||
}) |
||||
} |
||||
// 获取验证图片以及 token
|
||||
export const getCode = (data) => { |
||||
return request.postOriginal({ url: '/admin-api/system/captcha/get', data }) |
||||
} |
||||
|
||||
// 滑动或者点选验证
|
||||
export const reqCheck = (data) => { |
||||
return request.postOriginal({ url: '/admin-api/system/captcha/check', data }) |
||||
} |
||||
|
||||
// 获取应用信息
|
||||
export const getAppInfo = (instanceId: number) => { |
||||
return request.get({ |
||||
url: '/admin-api/system/serviceInstance/getInstanceInfo?instanceId=' + instanceId |
||||
}) |
||||
} |
@ -0,0 +1,41 @@ |
||||
import request from '@/config/axios' |
||||
|
||||
// 获得授权信息
|
||||
export const getAuthorize = (clientId: string) => { |
||||
return request.get({ url: '/admin-api/system/oauth2/authorize?clientId=' + clientId }) |
||||
} |
||||
|
||||
// 发起授权
|
||||
export const authorize = ( |
||||
responseType: string, |
||||
clientId: string, |
||||
redirectUri: string, |
||||
state: string, |
||||
autoApprove: boolean, |
||||
checkedScopes: string[], |
||||
uncheckedScopes: string[] |
||||
) => { |
||||
// 构建 scopes
|
||||
const scopes = {} |
||||
for (const scope of checkedScopes) { |
||||
scopes[scope] = true |
||||
} |
||||
for (const scope of uncheckedScopes) { |
||||
scopes[scope] = false |
||||
} |
||||
// 发起请求
|
||||
return request.post({ |
||||
url: '/admin-api/system/oauth2/authorize', |
||||
headers: { |
||||
'Content-type': 'application/x-www-form-urlencoded' |
||||
}, |
||||
params: { |
||||
response_type: responseType, |
||||
client_id: clientId, |
||||
redirect_uri: redirectUri, |
||||
state: state, |
||||
auto_approve: autoApprove, |
||||
scope: JSON.stringify(scopes) |
||||
} |
||||
}) |
||||
} |
@ -0,0 +1,28 @@ |
||||
export type UserLoginVO = { |
||||
username: string |
||||
password: string |
||||
captchaVerification: string |
||||
} |
||||
|
||||
export type TokenType = { |
||||
id: number // 编号
|
||||
accessToken: string // 访问令牌
|
||||
refreshToken: string // 刷新令牌
|
||||
userId: number // 用户编号
|
||||
userType: number //用户类型
|
||||
clientId: string //客户端编号
|
||||
expiresTime: number //过期时间
|
||||
} |
||||
|
||||
export type UserVO = { |
||||
id: number |
||||
username: string |
||||
nickname: string |
||||
deptId: number |
||||
email: string |
||||
mobile: string |
||||
sex: number |
||||
avatar: string |
||||
loginIp: string |
||||
loginDate: string |
||||
} |
@ -0,0 +1,5 @@ |
||||
import request from '@/config/axios' |
||||
|
||||
export const getSimpleAppList = async () => { |
||||
return await request.get({ url: '/admin-api/system/serviceInstance/simple-list' }) |
||||
} |
@ -0,0 +1,43 @@ |
||||
import request from '@/config/axios' |
||||
|
||||
export interface DeptVO { |
||||
id?: number |
||||
name: string |
||||
parentId: number |
||||
status: number |
||||
sort: number |
||||
leaderUserId: number |
||||
phone: string |
||||
email: string |
||||
createTime: Date |
||||
} |
||||
|
||||
// 查询部门(精简)列表
|
||||
export const getSimpleDeptList = async (params: any): Promise<any[]> => { |
||||
return await request.get({ url: '/admin-api/system/dept/list-all-simple', params }) |
||||
} |
||||
|
||||
// 查询部门列表
|
||||
export const getDeptPage = async (params) => { |
||||
return await request.get({ url: '/admin-api/system/dept/list', params }) |
||||
} |
||||
|
||||
// 查询部门详情
|
||||
export const getDept = async (id: number) => { |
||||
return await request.get({ url: '/admin-api/system/dept/get?id=' + id }) |
||||
} |
||||
|
||||
// 新增部门
|
||||
export const createDept = async (data: DeptVO) => { |
||||
return await request.post({ url: '/admin-api/system/dept/create', data: data }) |
||||
} |
||||
|
||||
// 修改部门
|
||||
export const updateDept = async (params: DeptVO) => { |
||||
return await request.put({ url: '/admin-api/system/dept/update', data: params }) |
||||
} |
||||
|
||||
// 删除部门
|
||||
export const deleteDept = async (id: number) => { |
||||
return await request.delete({ url: '/admin-api/system/dept/delete?id=' + id }) |
||||
} |
@ -0,0 +1,54 @@ |
||||
import request from '@/config/axios' |
||||
|
||||
export type DictDataVO = { |
||||
id: number | undefined |
||||
sort: number | undefined |
||||
label: string |
||||
value: string |
||||
dictType: string |
||||
status: number |
||||
colorType: string |
||||
cssClass: string |
||||
remark: string |
||||
createTime: Date |
||||
} |
||||
|
||||
// 查询字典数据(精简)列表
|
||||
export const listSimpleDictData = () => { |
||||
return request.get({ url: '/admin-api/oa/dict-data/simple-list' }) |
||||
} |
||||
|
||||
// 查询字典数据列表
|
||||
export const getDictDataPage = (params: PageParam) => { |
||||
return request.get({ url: '/admin-api/oa/dict-data/page', params }) |
||||
} |
||||
|
||||
// 查询字典数据详情
|
||||
export const getDictData = (id: number) => { |
||||
return request.get({ url: '/admin-api/oa/dict-data/get?id=' + id }) |
||||
} |
||||
|
||||
// 新增字典数据
|
||||
export const createDictData = (data: DictDataVO) => { |
||||
return request.post({ url: '/admin-api/oa/dict-data/create', data }) |
||||
} |
||||
|
||||
// 修改字典数据
|
||||
export const updateDictData = (data: DictDataVO) => { |
||||
return request.put({ url: '/admin-api/oa/dict-data/update', data }) |
||||
} |
||||
|
||||
// 删除字典数据
|
||||
export const deleteDictData = (id: number) => { |
||||
return request.delete({ url: '/admin-api/oa/dict-data/delete?id=' + id }) |
||||
} |
||||
|
||||
// 导出字典类型数据
|
||||
export const exportDictData = (params) => { |
||||
return request.get({ url: '/admin-api/oa/dict-data/export', params }) |
||||
} |
||||
|
||||
// 获取通用字典数据
|
||||
export const getGeneralSysDictData = (dictType: string) => { |
||||
return request.get({ url: '/admin-api/system/dict-data/get-by-type', params: { dictType } }) |
||||
} |
@ -0,0 +1,44 @@ |
||||
import request from '@/config/axios' |
||||
|
||||
export type DictTypeVO = { |
||||
id: number | undefined |
||||
name: string |
||||
type: string |
||||
status: number |
||||
remark: string |
||||
createTime: Date |
||||
} |
||||
|
||||
// 查询字典(精简)列表
|
||||
export const getSimpleDictTypeList = () => { |
||||
return request.get({ url: '/admin-api/oa/dict-type/list-all-simple' }) |
||||
} |
||||
|
||||
// 查询字典列表
|
||||
export const getDictTypePage = (params: PageParam) => { |
||||
return request.get({ url: '/admin-api/oa/dict-type/page', params }) |
||||
} |
||||
|
||||
// 查询字典详情
|
||||
export const getDictType = (id: number) => { |
||||
return request.get({ url: '/admin-api/oa/dict-type/get?id=' + id }) |
||||
} |
||||
|
||||
// 新增字典
|
||||
export const createDictType = (data: DictTypeVO) => { |
||||
return request.post({ url: '/admin-api/oa/dict-type/create', data }) |
||||
} |
||||
|
||||
// 修改字典
|
||||
export const updateDictType = (data: DictTypeVO) => { |
||||
return request.put({ url: '/admin-api/oa/dict-type/update', data }) |
||||
} |
||||
|
||||
// 删除字典
|
||||
export const deleteDictType = (id: number) => { |
||||
return request.delete({ url: '/admin-api/oa/dict-type/delete?id=' + id }) |
||||
} |
||||
// 导出字典类型
|
||||
export const exportDictType = (params) => { |
||||
return request.get({ url: '/admin-api/oa/dict-type/export', params }) |
||||
} |
@ -0,0 +1,54 @@ |
||||
import request from '@/config/axios' |
||||
|
||||
export interface MenuVO { |
||||
id: number |
||||
name: string |
||||
permission: string |
||||
type: number |
||||
sort: number |
||||
parentId: number |
||||
path: string |
||||
icon: string |
||||
component: string |
||||
componentName?: string |
||||
status: number |
||||
visible: boolean |
||||
keepAlive: boolean |
||||
alwaysShow?: boolean |
||||
createTime: Date |
||||
} |
||||
|
||||
// 获取服务列表
|
||||
export const getServiceAppList = () => { |
||||
return request.get({ url: '/admin-api/system/service/list' }) |
||||
} |
||||
|
||||
// 查询菜单(精简)列表
|
||||
export const getSimpleMenusList = () => { |
||||
return request.get({ url: '/admin-api/system/menu/list-all-simple' }) |
||||
} |
||||
|
||||
// 查询菜单列表
|
||||
export const getMenuList = (params) => { |
||||
return request.get({ url: '/admin-api/system/menu/list', params }) |
||||
} |
||||
|
||||
// 获取菜单详情
|
||||
export const getMenu = (id: number) => { |
||||
return request.get({ url: '/admin-api/system/menu/get?id=' + id }) |
||||
} |
||||
|
||||
// 新增菜单
|
||||
export const createMenu = (data: MenuVO) => { |
||||
return request.post({ url: '/admin-api/system/menu/create', data }) |
||||
} |
||||
|
||||
// 修改菜单
|
||||
export const updateMenu = (data: MenuVO) => { |
||||
return request.put({ url: '/admin-api/system/menu/update', data }) |
||||
} |
||||
|
||||
// 删除菜单
|
||||
export const deleteMenu = (id: number) => { |
||||
return request.delete({ url: '/admin-api/system/menu/delete?id=' + id }) |
||||
} |
@ -0,0 +1,53 @@ |
||||
import request from '@/config/axios' |
||||
|
||||
export interface NotifyMessageVO { |
||||
id: number |
||||
userId: number |
||||
userType: number |
||||
templateId: number |
||||
templateCode: string |
||||
templateNickname: string |
||||
templateContent: string |
||||
templateType: number |
||||
templateParams: string |
||||
readStatus: boolean |
||||
readTime: Date |
||||
} |
||||
|
||||
// 查询站内信消息列表
|
||||
export const getNotifyMessagePage = async (params: any) => { |
||||
return await request.get({ url: '/admin-api/system/notify-message/page', params }) |
||||
} |
||||
|
||||
// 获得我的站内信分页
|
||||
export const getMyNotifyMessagePage = async (params: any) => { |
||||
return await request.get({ url: '/admin-api/system/notify-message/my-page', params }) |
||||
} |
||||
|
||||
// 批量标记已读
|
||||
export const updateNotifyMessageRead = async (data: any) => { |
||||
return await request.put({ |
||||
url: '/admin-api/system/notify-message/update-read?', |
||||
data |
||||
}) |
||||
} |
||||
|
||||
// 标记所有站内信为已读
|
||||
export const updateAllNotifyMessageRead = async (data: any) => { |
||||
return await request.put({ url: '/admin-api/system/notify-message/update-all-read', data }) |
||||
} |
||||
|
||||
// 获取当前用户的最新站内信列表
|
||||
export const getUnreadNotifyMessageList = async (params: any) => { |
||||
return await request.get({ url: '/admin-api/system/notify-message/get-unread-list', params }) |
||||
} |
||||
|
||||
// 获得当前用户的未读站内信数量
|
||||
export const getUnreadNotifyMessageCount = async (params: any) => { |
||||
return await request.get({ url: '/admin-api/system/notify-message/get-unread-count', params }) |
||||
} |
||||
|
||||
// 获取详情
|
||||
export const getNotifyMessageDetail = async (id: number) => { |
||||
return await request.get({ url: '/admin-api/system/notify-message/get', params: { id } }) |
||||
} |
@ -0,0 +1,49 @@ |
||||
import request from '@/config/axios' |
||||
|
||||
export interface NotifyTemplateVO { |
||||
id?: number |
||||
name: string |
||||
nickname: string |
||||
code: string |
||||
content: string |
||||
type: number |
||||
params: string |
||||
status: number |
||||
remark: string |
||||
} |
||||
|
||||
export interface NotifySendReqVO { |
||||
userId: number | null |
||||
templateCode: string |
||||
templateParams: Map<String, Object> |
||||
} |
||||
|
||||
// 查询站内信模板列表
|
||||
export const getNotifyTemplatePage = async (params: PageParam) => { |
||||
return await request.get({ url: '/system/notify-template/page', params }) |
||||
} |
||||
|
||||
// 查询站内信模板详情
|
||||
export const getNotifyTemplate = async (id: number) => { |
||||
return await request.get({ url: '/system/notify-template/get?id=' + id }) |
||||
} |
||||
|
||||
// 新增站内信模板
|
||||
export const createNotifyTemplate = async (data: NotifyTemplateVO) => { |
||||
return await request.post({ url: '/system/notify-template/create', data }) |
||||
} |
||||
|
||||
// 修改站内信模板
|
||||
export const updateNotifyTemplate = async (data: NotifyTemplateVO) => { |
||||
return await request.put({ url: '/system/notify-template/update', data }) |
||||
} |
||||
|
||||
// 删除站内信模板
|
||||
export const deleteNotifyTemplate = async (id: number) => { |
||||
return await request.delete({ url: '/system/notify-template/delete?id=' + id }) |
||||
} |
||||
|
||||
// 发送站内信
|
||||
export const sendNotify = (data: NotifySendReqVO) => { |
||||
return request.post({ url: '/system/notify-template/send-notify', data }) |
||||
} |
@ -0,0 +1,42 @@ |
||||
import request from '@/config/axios' |
||||
|
||||
export interface PermissionAssignUserRoleReqVO { |
||||
userId: number |
||||
roleIds: number[] |
||||
} |
||||
|
||||
export interface PermissionAssignRoleMenuReqVO { |
||||
roleId: number |
||||
menuIds: number[] |
||||
} |
||||
|
||||
export interface PermissionAssignRoleDataScopeReqVO { |
||||
roleId: number |
||||
dataScope: number |
||||
dataScopeDeptIds: number[] |
||||
} |
||||
|
||||
// 查询角色拥有的菜单权限
|
||||
export const getRoleMenuList = async (roleId: number) => { |
||||
return await request.get({ url: '/admin-api/system/permission/list-role-menus?roleId=' + roleId }) |
||||
} |
||||
|
||||
// 赋予角色菜单权限
|
||||
export const assignRoleMenu = async (data: PermissionAssignRoleMenuReqVO) => { |
||||
return await request.post({ url: '/admin-api/system/permission/assign-role-menu', data }) |
||||
} |
||||
|
||||
// 赋予角色数据权限
|
||||
export const assignRoleDataScope = async (data: PermissionAssignRoleDataScopeReqVO) => { |
||||
return await request.post({ url: '/admin-api/system/permission/assign-role-data-scope', data }) |
||||
} |
||||
|
||||
// 查询用户拥有的角色数组
|
||||
export const getUserRoleList = async (userId: number) => { |
||||
return await request.get({ url: '/admin-api/system/permission/list-user-roles?userId=' + userId }) |
||||
} |
||||
|
||||
// 赋予用户角色
|
||||
export const assignUserRole = async (data: PermissionAssignUserRoleReqVO) => { |
||||
return await request.post({ url: '/admin-api/system/permission/assign-user-role', data }) |
||||
} |
@ -0,0 +1,53 @@ |
||||
import request from '@/config/axios' |
||||
|
||||
export interface RoleVO { |
||||
id: number |
||||
name: string |
||||
code: string |
||||
sort: number |
||||
status: number |
||||
type: number |
||||
dataScope: number |
||||
dataScopeDeptIds: number[] |
||||
createTime: Date |
||||
} |
||||
|
||||
export interface UpdateStatusReqVO { |
||||
id: number |
||||
status: number |
||||
} |
||||
|
||||
// 查询角色列表
|
||||
export const getRolePage = async (params: PageParam) => { |
||||
return await request.get({ url: '/admin-api/system/role/page', params }) |
||||
} |
||||
|
||||
// 查询角色(精简)列表
|
||||
export const getSimpleRoleList = async () => { |
||||
return await request.get({ url: '/admin-api/system/role/list-all-simple' }) |
||||
} |
||||
|
||||
// 查询角色详情
|
||||
export const getRole = async (id: number) => { |
||||
return await request.get({ url: '/admin-api/system/role/get?id=' + id }) |
||||
} |
||||
|
||||
// 新增角色
|
||||
export const createRole = async (data: RoleVO) => { |
||||
return await request.post({ url: '/admin-api/system/role/create', data }) |
||||
} |
||||
|
||||
// 修改角色
|
||||
export const updateRole = async (data: RoleVO) => { |
||||
return await request.put({ url: '/admin-api/system/role/update', data }) |
||||
} |
||||
|
||||
// 删除角色
|
||||
export const deleteRole = async (id: number) => { |
||||
return await request.delete({ url: '/admin-api/system/role/delete?id=' + id }) |
||||
} |
||||
|
||||
// 角色用户
|
||||
export const getRoleUsers = async (params) => { |
||||
return await request.get({ url: '/admin-api/system/role/getUserByRole', params }) |
||||
} |
@ -0,0 +1,16 @@ |
||||
import request from '@/config/axios' |
||||
|
||||
// 通过key,查询内容
|
||||
export const getConfigByConfigKey = (params) => { |
||||
return request.get({ url: '/admin-api/crm/config/getConfigByConfigKey', params }) |
||||
} |
||||
|
||||
// 保存配置项
|
||||
export const updateConfig = (data) => { |
||||
return request.put({ url: '/admin-api/crm/config/batchUpdateConfigValue', data }) |
||||
} |
||||
|
||||
// 根据模块获取配置列表
|
||||
export const getConfigList = (params) => { |
||||
return request.get({ url: '/admin-api/crm/config/query', params }) |
||||
} |
@ -0,0 +1,76 @@ |
||||
import request from '@/config/axios' |
||||
|
||||
export interface UserVO { |
||||
id: number |
||||
username: string |
||||
nickname: string |
||||
deptId: number |
||||
postIds: string[] |
||||
email: string |
||||
mobile: string |
||||
sex: number |
||||
avatar: string |
||||
loginIp: string |
||||
status: number |
||||
remark: string |
||||
loginDate: Date |
||||
createTime: Date |
||||
} |
||||
|
||||
// 查询用户管理列表
|
||||
export const getUserPage = (params: PageParam) => { |
||||
return request.get({ url: '/admin-api/system/user/page', params }) |
||||
} |
||||
|
||||
// 查询用户详情
|
||||
export const getUser = (id: number) => { |
||||
return request.get({ url: '/admin-api/system/user/get?id=' + id }) |
||||
} |
||||
|
||||
// 新增用户
|
||||
export const createUser = (data: UserVO) => { |
||||
return request.post({ url: '/admin-api/system/user/create', data }) |
||||
} |
||||
|
||||
// 修改用户
|
||||
export const updateUser = (data: UserVO) => { |
||||
return request.put({ url: '/admin-api/system/user/update', data }) |
||||
} |
||||
|
||||
// 删除用户
|
||||
export const deleteUser = (id: number) => { |
||||
return request.delete({ url: '/admin-api/system/user/delete?id=' + id }) |
||||
} |
||||
|
||||
// 导出用户
|
||||
export const exportUser = (params) => { |
||||
return request.download({ url: '/admin-api/system/user/export', params }) |
||||
} |
||||
|
||||
// 下载用户导入模板
|
||||
export const importUserTemplate = () => { |
||||
return request.download({ url: '/admin-api/system/user/get-import-template' }) |
||||
} |
||||
|
||||
// 用户密码重置
|
||||
export const resetUserPwd = (id: number, password: string) => { |
||||
const data = { |
||||
id, |
||||
password |
||||
} |
||||
return request.put({ url: '/admin-api/system/user/update-password', data: data }) |
||||
} |
||||
|
||||
// 用户状态修改
|
||||
export const updateUserStatus = (id: number, status: number) => { |
||||
const data = { |
||||
id, |
||||
status |
||||
} |
||||
return request.put({ url: '/admin-api/system/user/update-status', data: data }) |
||||
} |
||||
|
||||
// 获取用户精简信息列表
|
||||
export const getSimpleUserList = (): Promise<UserVO[]> => { |
||||
return request.get({ url: '/admin-api/system/user/list-all-simple' }) |
||||
} |
@ -0,0 +1,77 @@ |
||||
import request from '@/config/axios' |
||||
|
||||
export interface ProfileDept { |
||||
id: number |
||||
name: string |
||||
} |
||||
export interface ProfileRole { |
||||
id: number |
||||
name: string |
||||
} |
||||
export interface ProfilePost { |
||||
id: number |
||||
name: string |
||||
} |
||||
export interface SocialUser { |
||||
id: number |
||||
type: number |
||||
openid: string |
||||
token: string |
||||
rawTokenInfo: string |
||||
nickname: string |
||||
avatar: string |
||||
rawUserInfo: string |
||||
code: string |
||||
state: string |
||||
} |
||||
export interface ProfileVO { |
||||
id: number |
||||
username: string |
||||
nickname: string |
||||
dept: ProfileDept |
||||
roles: ProfileRole[] |
||||
posts: ProfilePost[] |
||||
socialUsers: SocialUser[] |
||||
email: string |
||||
mobile: string |
||||
sex: number |
||||
avatar: string |
||||
status: number |
||||
remark: string |
||||
loginIp: string |
||||
loginDate: Date |
||||
createTime: Date |
||||
} |
||||
|
||||
export interface UserProfileUpdateReqVO { |
||||
nickname: string |
||||
email: string |
||||
mobile: string |
||||
sex: number |
||||
} |
||||
|
||||
// 查询用户个人信息
|
||||
export const getUserProfile = () => { |
||||
return request.get({ url: '/admin-api/system/user/profile/get' }) |
||||
} |
||||
|
||||
// 修改用户个人信息
|
||||
export const updateUserProfile = (data: UserProfileUpdateReqVO) => { |
||||
return request.put({ url: '/admin-api/system/user/profile/update', data }) |
||||
} |
||||
|
||||
// 用户密码重置
|
||||
export const updateUserPassword = (oldPassword: string, newPassword: string) => { |
||||
return request.put({ |
||||
url: '/admin-api/system/user/profile/update-password', |
||||
data: { |
||||
oldPassword: oldPassword, |
||||
newPassword: newPassword |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// 用户头像上传
|
||||
export const uploadAvatar = (data) => { |
||||
return request.upload({ url: '/admin-api/system/user/profile/update-avatar', data: data }) |
||||
} |
@ -0,0 +1,31 @@ |
||||
import request from '@/config/axios' |
||||
|
||||
// 社交绑定,使用 code 授权码
|
||||
export const socialBind = (type, code, state) => { |
||||
return request.post({ |
||||
url: '/admin-api/system/social-user/bind', |
||||
data: { |
||||
type, |
||||
code, |
||||
state |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// 取消社交绑定
|
||||
export const socialUnbind = (type, openid) => { |
||||
return request.delete({ |
||||
url: '/admin-api/system/social-user/unbind', |
||||
data: { |
||||
type, |
||||
openid |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// 社交授权的跳转
|
||||
export const socialAuthRedirect = (type, redirectUri) => { |
||||
return request.get({ |
||||
url: '/admin-api/system/auth/social-auth-redirect?type=' + type + '&redirectUri=' + redirectUri |
||||
}) |
||||
} |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 309 KiB |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 192 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 669 B |
After Width: | Height: | Size: 335 B |
After Width: | Height: | Size: 731 B |
After Width: | Height: | Size: 2.2 KiB |
@ -0,0 +1,3 @@ |
||||
import Backtop from './src/Backtop.vue' |
||||
|
||||
export { Backtop } |
@ -0,0 +1,15 @@ |
||||
<script lang="ts" name="BackTop" setup> |
||||
import { ElBacktop } from 'element-plus' |
||||
import { useDesign } from '@/hooks/web/useDesign' |
||||
|
||||
const { getPrefixCls, variables } = useDesign() |
||||
|
||||
const prefixCls = getPrefixCls('backtop') |
||||
</script> |
||||
|
||||
<template> |
||||
<ElBacktop |
||||
:class="`${prefixCls}-backtop`" |
||||
:target="`.${variables.namespace}-layout-content-scrollbar .${variables.elNamespace}-scrollbar__wrap`" |
||||
/> |
||||
</template> |
@ -0,0 +1,3 @@ |
||||
import ConfigGlobal from './src/ConfigGlobal.vue' |
||||
|
||||
export { ConfigGlobal } |
@ -0,0 +1,61 @@ |
||||
<script lang="ts" name="ConfigGlobal" setup> |
||||
import { propTypes } from '@/utils/propTypes' |
||||
import { useLocaleStore } from '@/store/modules/locale' |
||||
import { useAppStore } from '@/store/modules/app' |
||||
import { setCssVar } from '@/utils' |
||||
import { useDesign } from '@/hooks/web/useDesign' |
||||
import { ElementPlusSize } from '@/types/elementPlus' |
||||
import { useWindowSize } from '@vueuse/core' |
||||
|
||||
const { variables } = useDesign() |
||||
|
||||
const appStore = useAppStore() |
||||
|
||||
const props = defineProps({ |
||||
size: propTypes.oneOf<ElementPlusSize>(['default', 'small', 'large']).def('default') |
||||
}) |
||||
|
||||
provide('configGlobal', props) |
||||
|
||||
// 初始化所有主题色 |
||||
onMounted(() => { |
||||
appStore.setCssVarTheme() |
||||
}) |
||||
|
||||
const { width } = useWindowSize() |
||||
|
||||
// 监听窗口变化 |
||||
watch( |
||||
() => width.value, |
||||
(width: number) => { |
||||
if (width < 768) { |
||||
!appStore.getMobile ? appStore.setMobile(true) : undefined |
||||
setCssVar('--left-menu-min-width', '0') |
||||
appStore.setCollapse(true) |
||||
appStore.getLayout !== 'classic' ? appStore.setLayout('classic') : undefined |
||||
} else { |
||||
appStore.getMobile ? appStore.setMobile(false) : undefined |
||||
setCssVar('--left-menu-min-width', '64px') |
||||
} |
||||
}, |
||||
{ |
||||
immediate: true |
||||
} |
||||
) |
||||
|
||||
// 多语言相关 |
||||
const localeStore = useLocaleStore() |
||||
|
||||
const currentLocale = computed(() => localeStore.currentLocale) |
||||
</script> |
||||
|
||||
<template> |
||||
<ElConfigProvider |
||||
:locale="currentLocale.elLocale" |
||||
:message="{ max: 1 }" |
||||
:namespace="variables.elNamespace" |
||||
:size="size" |
||||
> |
||||
<slot></slot> |
||||
</ElConfigProvider> |
||||
</template> |
@ -0,0 +1,3 @@ |
||||
import ContentDetailWrap from './src/ContentDetailWrap.vue' |
||||
|
||||
export { ContentDetailWrap } |
@ -0,0 +1,56 @@ |
||||
<script lang="ts" name="ContentDetailWrap" setup> |
||||
import { propTypes } from '@/utils/propTypes' |
||||
import { useDesign } from '@/hooks/web/useDesign' |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
const { getPrefixCls } = useDesign() |
||||
|
||||
const prefixCls = getPrefixCls('content-detail-wrap') |
||||
|
||||
defineProps({ |
||||
title: propTypes.string.def(''), |
||||
message: propTypes.string.def('') |
||||
}) |
||||
const emit = defineEmits(['back']) |
||||
const offset = ref(85) |
||||
const contentDetailWrap = ref() |
||||
onMounted(() => { |
||||
offset.value = contentDetailWrap.value.getBoundingClientRect().top |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div ref="contentDetailWrap" :class="[`${prefixCls}-container`]"> |
||||
<Sticky :offset="offset"> |
||||
<div |
||||
:class="[ |
||||
`${prefixCls}-header`, |
||||
'flex border-bottom-1 h-50px items-center text-center pr-10px' |
||||
]" |
||||
> |
||||
<div :class="[`${prefixCls}-header__back`, 'flex pl-10px pr-10px ']"> |
||||
<ElButton @click="emit('back')"> |
||||
<Icon class="mr-5px" icon="ep:arrow-left" /> |
||||
{{ t('common.back') }} |
||||
</ElButton> |
||||
</div> |
||||
<div :class="[`${prefixCls}-header__title`, 'flex flex-1 justify-center']"> |
||||
<slot name="title"> |
||||
<label class="text-16px font-700">{{ title }}</label> |
||||
</slot> |
||||
</div> |
||||
<div :class="[`${prefixCls}-header__right`, 'flex pl-10px pr-10px']"> |
||||
<slot name="right"></slot> |
||||
</div> |
||||
</div> |
||||
</Sticky> |
||||
<div style="padding: var(--app-content-padding)"> |
||||
<ElCard :class="[`${prefixCls}-body`, 'mb-20px']" shadow="never"> |
||||
<div> |
||||
<slot></slot> |
||||
</div> |
||||
</ElCard> |
||||
</div> |
||||
</div> |
||||
</template> |
@ -0,0 +1,3 @@ |
||||
import ContentWrap from './src/ContentWrap.vue' |
||||
|
||||
export { ContentWrap } |
@ -0,0 +1,32 @@ |
||||
<script lang="ts" name="ContentWrap" setup> |
||||
import { propTypes } from '@/utils/propTypes' |
||||
import { useDesign } from '@/hooks/web/useDesign' |
||||
|
||||
const { getPrefixCls } = useDesign() |
||||
|
||||
const prefixCls = getPrefixCls('content-wrap') |
||||
|
||||
defineProps({ |
||||
title: propTypes.string.def(''), |
||||
message: propTypes.string.def('') |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<ElCard :class="[prefixCls, 'mb-15px']" shadow="never"> |
||||
<template v-if="title" #header> |
||||
<div class="flex items-center"> |
||||
<span class="text-16px font-700">{{ title }}</span> |
||||
<ElTooltip v-if="message" effect="dark" placement="right"> |
||||
<template #content> |
||||
<div class="max-w-200px">{{ message }}</div> |
||||
</template> |
||||
<Icon :size="14" class="ml-5px" icon="ep:question-filled" /> |
||||
</ElTooltip> |
||||
</div> |
||||
</template> |
||||
<div> |
||||
<slot></slot> |
||||
</div> |
||||
</ElCard> |
||||
</template> |
@ -0,0 +1,3 @@ |
||||
import CountTo from './src/CountTo.vue' |
||||
|
||||
export { CountTo } |
@ -0,0 +1,180 @@ |
||||
<script lang="ts" name="CountTo" setup> |
||||
import { PropType } from 'vue' |
||||
import { isNumber } from '@/utils/is' |
||||
import { propTypes } from '@/utils/propTypes' |
||||
import { useDesign } from '@/hooks/web/useDesign' |
||||
|
||||
const { getPrefixCls } = useDesign() |
||||
|
||||
const prefixCls = getPrefixCls('count-to') |
||||
|
||||
const props = defineProps({ |
||||
startVal: propTypes.number.def(0), |
||||
endVal: propTypes.number.def(2021), |
||||
duration: propTypes.number.def(3000), |
||||
autoplay: propTypes.bool.def(true), |
||||
decimals: propTypes.number.validate((value: number) => value >= 0).def(0), |
||||
decimal: propTypes.string.def('.'), |
||||
separator: propTypes.string.def(','), |
||||
prefix: propTypes.string.def(''), |
||||
suffix: propTypes.string.def(''), |
||||
useEasing: propTypes.bool.def(true), |
||||
easingFn: { |
||||
type: Function as PropType<(t: number, b: number, c: number, d: number) => number>, |
||||
default(t: number, b: number, c: number, d: number) { |
||||
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b |
||||
} |
||||
} |
||||
}) |
||||
|
||||
const emit = defineEmits(['mounted', 'callback']) |
||||
|
||||
const formatNumber = (num: number | string) => { |
||||
const { decimals, decimal, separator, suffix, prefix } = props |
||||
num = Number(num).toFixed(decimals) |
||||
num += '' |
||||
const x = num.split('.') |
||||
let x1 = x[0] |
||||
const x2 = x.length > 1 ? decimal + x[1] : '' |
||||
const rgx = /(\d+)(\d{3})/ |
||||
if (separator && !isNumber(separator)) { |
||||
while (rgx.test(x1)) { |
||||
x1 = x1.replace(rgx, '$1' + separator + '$2') |
||||
} |
||||
} |
||||
return prefix + x1 + x2 + suffix |
||||
} |
||||
|
||||
const state = reactive<{ |
||||
localStartVal: number |
||||
printVal: number | null |
||||
displayValue: string |
||||
paused: boolean |
||||
localDuration: number | null |
||||
startTime: number | null |
||||
timestamp: number | null |
||||
rAF: any |
||||
remaining: number | null |
||||
}>({ |
||||
localStartVal: props.startVal, |
||||
displayValue: formatNumber(props.startVal), |
||||
printVal: null, |
||||
paused: false, |
||||
localDuration: props.duration, |
||||
startTime: null, |
||||
timestamp: null, |
||||
remaining: null, |
||||
rAF: null |
||||
}) |
||||
|
||||
const displayValue = toRef(state, 'displayValue') |
||||
|
||||
onMounted(() => { |
||||
if (props.autoplay) { |
||||
start() |
||||
} |
||||
emit('mounted') |
||||
}) |
||||
|
||||
const getCountDown = computed(() => { |
||||
return props.startVal > props.endVal |
||||
}) |
||||
|
||||
watch([() => props.startVal, () => props.endVal], () => { |
||||
if (props.autoplay) { |
||||
start() |
||||
} |
||||
}) |
||||
|
||||
const start = () => { |
||||
const { startVal, duration } = props |
||||
state.localStartVal = startVal |
||||
state.startTime = null |
||||
state.localDuration = duration |
||||
state.paused = false |
||||
state.rAF = requestAnimationFrame(count) |
||||
} |
||||
|
||||
const pauseResume = () => { |
||||
if (state.paused) { |
||||
resume() |
||||
state.paused = false |
||||
} else { |
||||
pause() |
||||
state.paused = true |
||||
} |
||||
} |
||||
|
||||
const pause = () => { |
||||
cancelAnimationFrame(state.rAF) |
||||
} |
||||
|
||||
const resume = () => { |
||||
state.startTime = null |
||||
state.localDuration = +(state.remaining as number) |
||||
state.localStartVal = +(state.printVal as number) |
||||
requestAnimationFrame(count) |
||||
} |
||||
|
||||
const reset = () => { |
||||
state.startTime = null |
||||
cancelAnimationFrame(state.rAF) |
||||
state.displayValue = formatNumber(props.startVal) |
||||
} |
||||
|
||||
const count = (timestamp: number) => { |
||||
const { useEasing, easingFn, endVal } = props |
||||
if (!state.startTime) state.startTime = timestamp |
||||
state.timestamp = timestamp |
||||
const progress = timestamp - state.startTime |
||||
state.remaining = (state.localDuration as number) - progress |
||||
if (useEasing) { |
||||
if (unref(getCountDown)) { |
||||
state.printVal = |
||||
state.localStartVal - |
||||
easingFn(progress, 0, state.localStartVal - endVal, state.localDuration as number) |
||||
} else { |
||||
state.printVal = easingFn( |
||||
progress, |
||||
state.localStartVal, |
||||
endVal - state.localStartVal, |
||||
state.localDuration as number |
||||
) |
||||
} |
||||
} else { |
||||
if (unref(getCountDown)) { |
||||
state.printVal = |
||||
state.localStartVal - |
||||
(state.localStartVal - endVal) * (progress / (state.localDuration as number)) |
||||
} else { |
||||
state.printVal = |
||||
state.localStartVal + |
||||
(endVal - state.localStartVal) * (progress / (state.localDuration as number)) |
||||
} |
||||
} |
||||
if (unref(getCountDown)) { |
||||
state.printVal = state.printVal < endVal ? endVal : state.printVal |
||||
} else { |
||||
state.printVal = state.printVal > endVal ? endVal : state.printVal |
||||
} |
||||
state.displayValue = formatNumber(state.printVal!) |
||||
if (progress < (state.localDuration as number)) { |
||||
state.rAF = requestAnimationFrame(count) |
||||
} else { |
||||
emit('callback') |
||||
} |
||||
} |
||||
|
||||
defineExpose({ |
||||
pauseResume, |
||||
reset, |
||||
start, |
||||
pause |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<span :class="prefixCls"> |
||||
{{ displayValue }} |
||||
</span> |
||||
</template> |
@ -0,0 +1,2 @@ |
||||
import Crontab from './src/Crontab.vue' |
||||
export { Crontab } |
@ -0,0 +1,4 @@ |
||||
import CropperImage from './src/Cropper.vue' |
||||
import CropperAvatar from './src/CropperAvatar.vue' |
||||
|
||||
export { CropperImage, CropperAvatar } |
@ -0,0 +1,257 @@ |
||||
<template> |
||||
<div> |
||||
<Dialog |
||||
v-model="dialogVisible" |
||||
:canFullscreen="false" |
||||
:title="t('cropper.modalTitle')" |
||||
maxHeight="380px" |
||||
width="800px" |
||||
> |
||||
<div :class="prefixCls"> |
||||
<div :class="`${prefixCls}-left`"> |
||||
<div :class="`${prefixCls}-cropper`"> |
||||
<CropperImage |
||||
v-if="src" |
||||
:circled="circled" |
||||
:src="src" |
||||
height="300px" |
||||
@cropend="handleCropend" |
||||
@ready="handleReady" |
||||
/> |
||||
</div> |
||||
|
||||
<div :class="`${prefixCls}-toolbar`"> |
||||
<el-upload :beforeUpload="handleBeforeUpload" :fileList="[]" accept="image/*"> |
||||
<el-tooltip :content="t('cropper.selectImage')" placement="bottom"> |
||||
<XButton preIcon="ant-design:upload-outlined" type="primary" /> |
||||
</el-tooltip> |
||||
</el-upload> |
||||
<el-space> |
||||
<el-tooltip :content="t('cropper.btn_reset')" placement="bottom"> |
||||
<XButton |
||||
:disabled="!src" |
||||
preIcon="ant-design:reload-outlined" |
||||
size="small" |
||||
type="primary" |
||||
@click="handlerToolbar('reset')" |
||||
/> |
||||
</el-tooltip> |
||||
<el-tooltip :content="t('cropper.btn_rotate_left')" placement="bottom"> |
||||
<XButton |
||||
:disabled="!src" |
||||
preIcon="ant-design:rotate-left-outlined" |
||||
size="small" |
||||
type="primary" |
||||
@click="handlerToolbar('rotate', -45)" |
||||
/> |
||||
</el-tooltip> |
||||
<el-tooltip :content="t('cropper.btn_rotate_right')" placement="bottom"> |
||||
<XButton |
||||
:disabled="!src" |
||||
preIcon="ant-design:rotate-right-outlined" |
||||
size="small" |
||||
type="primary" |
||||
@click="handlerToolbar('rotate', 45)" |
||||
/> |
||||
</el-tooltip> |
||||
<el-tooltip :content="t('cropper.btn_scale_x')" placement="bottom"> |
||||
<XButton |
||||
:disabled="!src" |
||||
preIcon="vaadin:arrows-long-h" |
||||
size="small" |
||||
type="primary" |
||||
@click="handlerToolbar('scaleX')" |
||||
/> |
||||
</el-tooltip> |
||||
<el-tooltip :content="t('cropper.btn_scale_y')" placement="bottom"> |
||||
<XButton |
||||
:disabled="!src" |
||||
preIcon="vaadin:arrows-long-v" |
||||
size="small" |
||||
type="primary" |
||||
@click="handlerToolbar('scaleY')" |
||||
/> |
||||
</el-tooltip> |
||||
<el-tooltip :content="t('cropper.btn_zoom_in')" placement="bottom"> |
||||
<XButton |
||||
:disabled="!src" |
||||
preIcon="ant-design:zoom-in-outlined" |
||||
size="small" |
||||
type="primary" |
||||
@click="handlerToolbar('zoom', 0.1)" |
||||
/> |
||||
</el-tooltip> |
||||
<el-tooltip :content="t('cropper.btn_zoom_out')" placement="bottom"> |
||||
<XButton |
||||
:disabled="!src" |
||||
preIcon="ant-design:zoom-out-outlined" |
||||
size="small" |
||||
type="primary" |
||||
@click="handlerToolbar('zoom', -0.1)" |
||||
/> |
||||
</el-tooltip> |
||||
</el-space> |
||||
</div> |
||||
</div> |
||||
<div :class="`${prefixCls}-right`"> |
||||
<div :class="`${prefixCls}-preview`"> |
||||
<img v-if="previewSource" :alt="t('cropper.preview')" :src="previewSource" /> |
||||
</div> |
||||
<template v-if="previewSource"> |
||||
<div :class="`${prefixCls}-group`"> |
||||
<el-avatar :src="previewSource" size="large" /> |
||||
<el-avatar :size="48" :src="previewSource" /> |
||||
<el-avatar :size="64" :src="previewSource" /> |
||||
<el-avatar :size="80" :src="previewSource" /> |
||||
</div> |
||||
</template> |
||||
</div> |
||||
</div> |
||||
<template #footer> |
||||
<el-button type="primary" @click="handleOk">{{ t('cropper.okText') }}</el-button> |
||||
</template> |
||||
</Dialog> |
||||
</div> |
||||
</template> |
||||
<script lang="ts" name="CopperModal" setup> |
||||
import { useDesign } from '@/hooks/web/useDesign' |
||||
import { dataURLtoBlob } from '@/utils/filt' |
||||
import { useI18n } from 'vue-i18n' |
||||
import type { CropendResult, Cropper } from './types' |
||||
import { propTypes } from '@/utils/propTypes' |
||||
import { CropperImage } from '@/components/Cropper' |
||||
|
||||
const props = defineProps({ |
||||
srcValue: propTypes.string.def(''), |
||||
circled: propTypes.bool.def(true) |
||||
}) |
||||
const emit = defineEmits(['uploadSuccess']) |
||||
const { t } = useI18n() |
||||
const { getPrefixCls } = useDesign() |
||||
const prefixCls = getPrefixCls('cropper-am') |
||||
|
||||
const src = ref(props.srcValue) |
||||
const previewSource = ref('') |
||||
const cropper = ref<Cropper>() |
||||
const dialogVisible = ref(false) |
||||
let filename = '' |
||||
let scaleX = 1 |
||||
let scaleY = 1 |
||||
|
||||
// Block upload |
||||
function handleBeforeUpload(file: File) { |
||||
const reader = new FileReader() |
||||
reader.readAsDataURL(file) |
||||
src.value = '' |
||||
previewSource.value = '' |
||||
reader.onload = function (e) { |
||||
src.value = (e.target?.result as string) ?? '' |
||||
filename = file.name |
||||
} |
||||
return false |
||||
} |
||||
|
||||
function handleCropend({ imgBase64 }: CropendResult) { |
||||
previewSource.value = imgBase64 |
||||
} |
||||
|
||||
function handleReady(cropperInstance: Cropper) { |
||||
cropper.value = cropperInstance |
||||
} |
||||
|
||||
function handlerToolbar(event: string, arg?: number) { |
||||
if (event === 'scaleX') { |
||||
scaleX = arg = scaleX === -1 ? 1 : -1 |
||||
} |
||||
if (event === 'scaleY') { |
||||
scaleY = arg = scaleY === -1 ? 1 : -1 |
||||
} |
||||
cropper?.value?.[event]?.(arg) |
||||
} |
||||
|
||||
async function handleOk() { |
||||
const blob = dataURLtoBlob(previewSource.value) |
||||
emit('uploadSuccess', { source: previewSource.value, data: blob, filename: filename }) |
||||
} |
||||
|
||||
function openModal() { |
||||
dialogVisible.value = true |
||||
} |
||||
|
||||
function closeModal() { |
||||
dialogVisible.value = false |
||||
} |
||||
|
||||
defineExpose({ openModal, closeModal }) |
||||
</script> |
||||
<style lang="scss"> |
||||
$prefix-cls: #{$namespace}-cropper-am; |
||||
|
||||
.#{$prefix-cls} { |
||||
display: flex; |
||||
|
||||
&-left, |
||||
&-right { |
||||
height: 340px; |
||||
} |
||||
|
||||
&-left { |
||||
width: 55%; |
||||
} |
||||
|
||||
&-right { |
||||
width: 45%; |
||||
} |
||||
|
||||
&-cropper { |
||||
height: 300px; |
||||
background: #eee; |
||||
background-image: linear-gradient( |
||||
45deg, |
||||
rgb(0 0 0 / 25%) 25%, |
||||
transparent 0, |
||||
transparent 75%, |
||||
rgb(0 0 0 / 25%) 0 |
||||
), |
||||
linear-gradient( |
||||
45deg, |
||||
rgb(0 0 0 / 25%) 25%, |
||||
transparent 0, |
||||
transparent 75%, |
||||
rgb(0 0 0 / 25%) 0 |
||||
); |
||||
background-position: 0 0, 12px 12px; |
||||
background-size: 24px 24px; |
||||
} |
||||
|
||||
&-toolbar { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
margin-top: 10px; |
||||
} |
||||
|
||||
&-preview { |
||||
width: 220px; |
||||
height: 220px; |
||||
margin: 0 auto; |
||||
overflow: hidden; |
||||
border: 1px solid; |
||||
border-radius: 50%; |
||||
|
||||
img { |
||||
width: 100%; |
||||
height: 100%; |
||||
} |
||||
} |
||||
|
||||
&-group { |
||||
display: flex; |
||||
padding-top: 8px; |
||||
margin-top: 8px; |
||||
border-top: 1px solid; |
||||
justify-content: space-around; |
||||
align-items: center; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,181 @@ |
||||
<template> |
||||
<div :class="getClass" :style="getWrapperStyle"> |
||||
<img |
||||
v-show="isReady" |
||||
ref="imgElRef" |
||||
:alt="alt" |
||||
:crossorigin="crossorigin" |
||||
:src="src" |
||||
:style="getImageStyle" |
||||
/> |
||||
</div> |
||||
</template> |
||||
<script lang="ts" name="Cropper" setup> |
||||
import { CSSProperties, PropType } from 'vue' |
||||
import Cropper from 'cropperjs' |
||||
import 'cropperjs/dist/cropper.css' |
||||
import { useDesign } from '@/hooks/web/useDesign' |
||||
import { propTypes } from '@/utils/propTypes' |
||||
import { useDebounceFn } from '@vueuse/core' |
||||
|
||||
type Options = Cropper.Options |
||||
|
||||
const defaultOptions: Options = { |
||||
aspectRatio: 1, |
||||
zoomable: true, |
||||
zoomOnTouch: true, |
||||
zoomOnWheel: true, |
||||
cropBoxMovable: true, |
||||
cropBoxResizable: true, |
||||
toggleDragModeOnDblclick: true, |
||||
autoCrop: true, |
||||
background: true, |
||||
highlight: true, |
||||
center: true, |
||||
responsive: true, |
||||
restore: true, |
||||
checkCrossOrigin: true, |
||||
checkOrientation: true, |
||||
scalable: true, |
||||
modal: true, |
||||
guides: true, |
||||
movable: true, |
||||
rotatable: true |
||||
} |
||||
|
||||
const props = defineProps({ |
||||
src: propTypes.string.def(''), |
||||
alt: propTypes.string.def(''), |
||||
circled: propTypes.bool.def(false), |
||||
realTimePreview: propTypes.bool.def(true), |
||||
height: propTypes.string.def('360px'), |
||||
crossorigin: { |
||||
type: String as PropType<'' | 'anonymous' | 'use-credentials' | undefined>, |
||||
default: undefined |
||||
}, |
||||
imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) }, |
||||
options: { type: Object as PropType<Options>, default: () => ({}) } |
||||
}) |
||||
|
||||
const emit = defineEmits(['cropend', 'ready', 'cropendError']) |
||||
const attrs = useAttrs() |
||||
const imgElRef = ref<ElRef<HTMLImageElement>>() |
||||
const cropper = ref<Nullable<Cropper>>() |
||||
const isReady = ref(false) |
||||
|
||||
const { getPrefixCls } = useDesign() |
||||
const prefixCls = getPrefixCls('cropper-image') |
||||
const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80) |
||||
|
||||
const getImageStyle = computed((): CSSProperties => { |
||||
return { |
||||
height: props.height, |
||||
maxWidth: '100%', |
||||
...props.imageStyle |
||||
} |
||||
}) |
||||
|
||||
const getClass = computed(() => { |
||||
return [ |
||||
prefixCls, |
||||
attrs.class, |
||||
{ |
||||
[`${prefixCls}--circled`]: props.circled |
||||
} |
||||
] |
||||
}) |
||||
const getWrapperStyle = computed((): CSSProperties => { |
||||
return { height: `${props.height}`.replace(/px/, '') + 'px' } |
||||
}) |
||||
|
||||
onMounted(init) |
||||
|
||||
onUnmounted(() => { |
||||
cropper.value?.destroy() |
||||
}) |
||||
|
||||
async function init() { |
||||
const imgEl = unref(imgElRef) |
||||
if (!imgEl) { |
||||
return |
||||
} |
||||
cropper.value = new Cropper(imgEl, { |
||||
...defaultOptions, |
||||
ready: () => { |
||||
isReady.value = true |
||||
realTimeCroppered() |
||||
emit('ready', cropper.value) |
||||
}, |
||||
crop() { |
||||
debounceRealTimeCroppered() |
||||
}, |
||||
zoom() { |
||||
debounceRealTimeCroppered() |
||||
}, |
||||
cropmove() { |
||||
debounceRealTimeCroppered() |
||||
}, |
||||
...props.options |
||||
}) |
||||
} |
||||
|
||||
// Real-time display preview |
||||
function realTimeCroppered() { |
||||
props.realTimePreview && croppered() |
||||
} |
||||
|
||||
// event: return base64 and width and height information after cropping |
||||
function croppered() { |
||||
if (!cropper.value) { |
||||
return |
||||
} |
||||
let imgInfo = cropper.value.getData() |
||||
const canvas = props.circled ? getRoundedCanvas() : cropper.value.getCroppedCanvas() |
||||
canvas.toBlob((blob) => { |
||||
if (!blob) { |
||||
return |
||||
} |
||||
let fileReader: FileReader = new FileReader() |
||||
fileReader.readAsDataURL(blob) |
||||
fileReader.onloadend = (e) => { |
||||
emit('cropend', { |
||||
imgBase64: e.target?.result ?? '', |
||||
imgInfo |
||||
}) |
||||
} |
||||
fileReader.onerror = () => { |
||||
emit('cropendError') |
||||
} |
||||
}, 'image/png') |
||||
} |
||||
|
||||
// Get a circular picture canvas |
||||
function getRoundedCanvas() { |
||||
const sourceCanvas = cropper.value!.getCroppedCanvas() |
||||
const canvas = document.createElement('canvas') |
||||
const context = canvas.getContext('2d')! |
||||
const width = sourceCanvas.width |
||||
const height = sourceCanvas.height |
||||
canvas.width = width |
||||
canvas.height = height |
||||
context.imageSmoothingEnabled = true |
||||
context.drawImage(sourceCanvas, 0, 0, width, height) |
||||
context.globalCompositeOperation = 'destination-in' |
||||
context.beginPath() |
||||
context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true) |
||||
context.fill() |
||||
return canvas |
||||
} |
||||
</script> |
||||
<style lang="scss"> |
||||
$prefix-cls: #{$namespace}-cropper-image; |
||||
|
||||
.#{$prefix-cls} { |
||||
&--circled { |
||||
.cropper-view-box, |
||||
.cropper-face { |
||||
border-radius: 50%; |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,141 @@ |
||||
<template> |
||||
<div class="user-info-head" @click="open()"> |
||||
<img v-if="sourceValue" :src="sourceValue" alt="avatar" class="img-circle img-lg" /> |
||||
<el-button v-if="showBtn" :class="`${prefixCls}-upload-btn`" @click="open()"> |
||||
{{ btnText ? btnText : t('cropper.selectImage') }} |
||||
</el-button> |
||||
<CopperModal |
||||
ref="cropperModelRef" |
||||
:srcValue="sourceValue" |
||||
@upload-success="handleUploadSuccess" |
||||
/> |
||||
</div> |
||||
</template> |
||||
<script lang="ts" name="CropperAvatar" setup> |
||||
import { useDesign } from '@/hooks/web/useDesign' |
||||
|
||||
import { propTypes } from '@/utils/propTypes' |
||||
import { useI18n } from 'vue-i18n' |
||||
import CopperModal from './CopperModal.vue' |
||||
|
||||
const props = defineProps({ |
||||
width: propTypes.string.def('200px'), |
||||
value: propTypes.string.def(''), |
||||
showBtn: propTypes.bool.def(true), |
||||
btnText: propTypes.string.def('') |
||||
}) |
||||
|
||||
const emit = defineEmits(['update:value', 'change']) |
||||
const sourceValue = ref(props.value) |
||||
const { getPrefixCls } = useDesign() |
||||
const prefixCls = getPrefixCls('cropper-avatar') |
||||
const message = useMessage() |
||||
const { t } = useI18n() |
||||
|
||||
const cropperModelRef = ref() |
||||
|
||||
watchEffect(() => { |
||||
sourceValue.value = props.value |
||||
}) |
||||
|
||||
watch( |
||||
() => sourceValue.value, |
||||
(v: string) => { |
||||
emit('update:value', v) |
||||
} |
||||
) |
||||
|
||||
function handleUploadSuccess({ source, data, filename }) { |
||||
sourceValue.value = source |
||||
emit('change', { source, data, filename }) |
||||
message.success(t('cropper.uploadSuccess')) |
||||
} |
||||
|
||||
function open() { |
||||
cropperModelRef.value.openModal() |
||||
} |
||||
|
||||
function close() { |
||||
cropperModelRef.value.closeModal() |
||||
} |
||||
|
||||
defineExpose({ |
||||
open, |
||||
close |
||||
}) |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
$prefix-cls: #{$namespace}--cropper-avatar; |
||||
|
||||
.#{$prefix-cls} { |
||||
display: inline-block; |
||||
text-align: center; |
||||
|
||||
&-image-wrapper { |
||||
overflow: hidden; |
||||
cursor: pointer; |
||||
border: 1px solid; |
||||
border-radius: 50%; |
||||
|
||||
img { |
||||
width: 100%; |
||||
} |
||||
} |
||||
|
||||
&-image-mask { |
||||
opacity: 0%; |
||||
position: absolute; |
||||
width: inherit; |
||||
height: inherit; |
||||
border-radius: inherit; |
||||
border: inherit; |
||||
background: rgb(0 0 0 / 40%); |
||||
cursor: pointer; |
||||
transition: opacity 0.4s; |
||||
|
||||
::v-deep(svg) { |
||||
margin: auto; |
||||
} |
||||
} |
||||
|
||||
&-image-mask:hover { |
||||
opacity: 4000%; |
||||
} |
||||
|
||||
&-upload-btn { |
||||
margin: 10px auto; |
||||
} |
||||
} |
||||
|
||||
.user-info-head { |
||||
position: relative; |
||||
display: inline-block; |
||||
} |
||||
|
||||
.img-circle { |
||||
border-radius: 50%; |
||||
} |
||||
|
||||
.img-lg { |
||||
width: 120px; |
||||
height: 120px; |
||||
} |
||||
|
||||
.user-info-head:hover:after { |
||||
content: '+'; |
||||
position: absolute; |
||||
left: 0; |
||||
right: 0; |
||||
top: 0; |
||||
bottom: 0; |
||||
color: #eee; |
||||
background: rgba(0, 0, 0, 0.5); |
||||
font-size: 24px; |
||||
font-style: normal; |
||||
-webkit-font-smoothing: antialiased; |
||||
-moz-osx-font-smoothing: grayscale; |
||||
cursor: pointer; |
||||
line-height: 110px; |
||||
border-radius: 50%; |
||||
} |
||||
</style> |
@ -0,0 +1,8 @@ |
||||
import type Cropper from 'cropperjs' |
||||
|
||||
export interface CropendResult { |
||||
imgBase64: string |
||||
imgInfo: Cropper.Data |
||||
} |
||||
|
||||
export type { Cropper } |
@ -0,0 +1,3 @@ |
||||
import Descriptions from './src/Descriptions.vue' |
||||
|
||||
export { Descriptions } |
@ -0,0 +1,172 @@ |
||||
<script lang="ts" name="Descriptions" setup> |
||||
import { PropType } from 'vue' |
||||
import dayjs from 'dayjs' |
||||
import { useDesign } from '@/hooks/web/useDesign' |
||||
import { propTypes } from '@/utils/propTypes' |
||||
import { useAppStore } from '@/store/modules/app' |
||||
import { DescriptionsSchema } from '@/types/descriptions' |
||||
|
||||
const appStore = useAppStore() |
||||
|
||||
const mobile = computed(() => appStore.getMobile) |
||||
|
||||
const attrs = useAttrs() |
||||
|
||||
const slots = useSlots() |
||||
|
||||
const props = defineProps({ |
||||
title: propTypes.string.def(''), |
||||
message: propTypes.string.def(''), |
||||
collapse: propTypes.bool.def(true), |
||||
columns: propTypes.number.def(1), |
||||
labelWidth: propTypes.string.def('100px'), |
||||
schema: { |
||||
type: Array as PropType<DescriptionsSchema[]>, |
||||
default: () => [] |
||||
}, |
||||
data: { |
||||
type: Object as PropType<any>, |
||||
default: () => ({}) |
||||
}, |
||||
defaultShow: propTypes.bool.def(true) |
||||
}) |
||||
|
||||
const { getPrefixCls } = useDesign() |
||||
|
||||
const prefixCls = getPrefixCls('descriptions') |
||||
|
||||
const getBindValue = computed(() => { |
||||
const delArr: string[] = ['title', 'message', 'collapse', 'schema', 'data', 'class'] |
||||
const obj = { ...attrs, ...props } |
||||
for (const key in obj) { |
||||
if (delArr.indexOf(key) !== -1) { |
||||
delete obj[key] |
||||
} |
||||
} |
||||
return obj |
||||
}) |
||||
|
||||
const getBindItemValue = (item: DescriptionsSchema) => { |
||||
const delArr: string[] = ['field'] |
||||
const obj = { ...item } |
||||
for (const key in obj) { |
||||
if (delArr.indexOf(key) !== -1) { |
||||
delete obj[key] |
||||
} |
||||
} |
||||
return obj |
||||
} |
||||
|
||||
// 折叠 |
||||
const show = ref(props.defaultShow) |
||||
|
||||
const toggleClick = () => { |
||||
if (props.collapse) { |
||||
show.value = !unref(show) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div |
||||
:class="[ |
||||
prefixCls, |
||||
'bg-[var(--el-color-white)] dark:(bg-[var(--el-bg-color)] border-[var(--el-border-color)] border-1px)' |
||||
]" |
||||
> |
||||
<div |
||||
v-if="title" |
||||
:class="[ |
||||
`${prefixCls}-header`, |
||||
'h-40px flex justify-between items-center border-bottom-1 border-solid border-[var(--tags-view-border-color)] px-10px cursor-pointer dark:border-[var(--el-border-color)]' |
||||
]" |
||||
@click="toggleClick" |
||||
> |
||||
<div :class="[`${prefixCls}-header__title`, 'relative font-18px font-bold ml-10px']"> |
||||
<div class="flex items-center"> |
||||
{{ title }} |
||||
<ElTooltip v-if="message" :content="message" placement="right"> |
||||
<Icon class="ml-5px" icon="ep:warning" /> |
||||
</ElTooltip> |
||||
</div> |
||||
</div> |
||||
<Icon v-if="collapse" :icon="show ? 'ep:arrow-down' : 'ep:arrow-up'" /> |
||||
</div> |
||||
|
||||
<ElCollapseTransition> |
||||
<div v-show="show" :class="[`${prefixCls}-content`, 'p-10px']"> |
||||
<ElDescriptions |
||||
:column="props.columns" |
||||
:direction="mobile ? 'vertical' : 'horizontal'" |
||||
border |
||||
v-bind="getBindValue" |
||||
> |
||||
<template v-if="slots['extra']" #extra> |
||||
<slot name="extra"></slot> |
||||
</template> |
||||
<ElDescriptionsItem |
||||
v-for="item in schema" |
||||
:key="item.field" |
||||
min-width="80" |
||||
:span="item.span" |
||||
label-class-name="desc-label" |
||||
v-bind="getBindItemValue(item)" |
||||
> |
||||
<template #label> |
||||
<slot |
||||
:name="`${item.field}-label`" |
||||
:row="{ |
||||
label: item.label |
||||
}" |
||||
>{{ item.label }} |
||||
</slot> |
||||
</template> |
||||
|
||||
<template #default> |
||||
<slot v-if="item.dateFormat"> |
||||
{{ |
||||
data[item.field] !== null ? dayjs(data[item.field]).format(item.dateFormat) : '' |
||||
}} |
||||
</slot> |
||||
<slot v-else-if="item.dictType"> |
||||
<DictTag :type="item.dictType" :value="data[item.field] + ''" /> |
||||
</slot> |
||||
<slot v-else-if="item.isEditor"> |
||||
<div v-if="data[item.field]" v-dompurify-html="data[item.field]"></div> |
||||
</slot> |
||||
<slot v-else :name="item.field" :row="data">{{ data[item.field] }}</slot> |
||||
</template> |
||||
</ElDescriptionsItem> |
||||
</ElDescriptions> |
||||
</div> |
||||
</ElCollapseTransition> |
||||
</div> |
||||
</template> |
||||
|
||||
<style lang="scss" scoped> |
||||
$prefix-cls: #{$namespace}-descriptions; |
||||
|
||||
.#{$prefix-cls}-header { |
||||
&__title { |
||||
&::after { |
||||
position: absolute; |
||||
top: 3px; |
||||
left: -10px; |
||||
width: 4px; |
||||
height: 70%; |
||||
background: var(--el-color-primary); |
||||
content: ''; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.#{$prefix-cls}-content { |
||||
// :deep(.#{$elNamespace}-descriptions__cell) { |
||||
// width: 0; |
||||
// } |
||||
|
||||
:deep(.desc-label) { |
||||
width: v-bind('labelWidth'); |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,3 @@ |
||||
import Dialog from './src/Dialog.vue' |
||||
|
||||
export { Dialog } |
@ -0,0 +1,126 @@ |
||||
<script lang="ts" name="Dialog" setup> |
||||
import { propTypes } from '@/utils/propTypes' |
||||
import { isNumber } from '@/utils/is' |
||||
|
||||
const slots = useSlots() |
||||
|
||||
const props = defineProps({ |
||||
modelValue: propTypes.bool.def(false), |
||||
title: propTypes.string.def('Dialog'), |
||||
fullscreen: propTypes.bool.def(true), |
||||
width: propTypes.oneOfType([String, Number]).def('40%'), |
||||
scroll: propTypes.bool.def(false), // 是否开启滚动条。如果是的话,按照 maxHeight 设置最大高度 |
||||
maxHeight: propTypes.oneOfType([String, Number]).def('300px') |
||||
}) |
||||
|
||||
const getBindValue = computed(() => { |
||||
const delArr: string[] = ['fullscreen', 'title', 'maxHeight'] |
||||
const attrs = useAttrs() |
||||
const obj = { ...attrs, ...props } |
||||
for (const key in obj) { |
||||
if (delArr.indexOf(key) !== -1) { |
||||
delete obj[key] |
||||
} |
||||
} |
||||
return obj |
||||
}) |
||||
|
||||
const isFullscreen = ref(false) |
||||
|
||||
const toggleFull = () => { |
||||
isFullscreen.value = !unref(isFullscreen) |
||||
} |
||||
|
||||
const dialogHeight = ref(isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight) |
||||
|
||||
watch( |
||||
() => isFullscreen.value, |
||||
async (val: boolean) => { |
||||
// 计算最大高度 |
||||
await nextTick() |
||||
if (val) { |
||||
const windowHeight = document.documentElement.offsetHeight |
||||
dialogHeight.value = `${windowHeight - 55 - 60 - (slots.footer ? 63 : 0)}px` |
||||
} else { |
||||
dialogHeight.value = isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight |
||||
} |
||||
}, |
||||
{ |
||||
immediate: true |
||||
} |
||||
) |
||||
|
||||
const dialogStyle = computed(() => { |
||||
return { |
||||
height: unref(dialogHeight) |
||||
} |
||||
}) |
||||
|
||||
const emit = defineEmits(['close']) |
||||
</script> |
||||
|
||||
<template> |
||||
<ElDialog |
||||
:close-on-click-modal="false" |
||||
:fullscreen="isFullscreen" |
||||
:width="width" |
||||
destroy-on-close |
||||
draggable |
||||
lock-scroll |
||||
v-bind="getBindValue" |
||||
@close="emit('close')" |
||||
> |
||||
<template #header> |
||||
<div class="flex justify-between"> |
||||
<slot name="title"> |
||||
{{ title }} |
||||
</slot> |
||||
<Icon |
||||
v-if="fullscreen" |
||||
:icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'" |
||||
class="mr-22px cursor-pointer is-hover mt-2px z-10" |
||||
color="var(--el-color-info)" |
||||
@click="toggleFull" |
||||
/> |
||||
</div> |
||||
</template> |
||||
|
||||
<!-- 情况一:如果 scroll 为 true,说明开启滚动条 --> |
||||
<ElScrollbar v-if="scroll" :style="dialogStyle"> |
||||
<slot></slot> |
||||
</ElScrollbar> |
||||
<!-- 情况二:如果 scroll 为 false,说明关闭滚动条滚动条 --> |
||||
<slot v-else></slot> |
||||
|
||||
<template v-if="slots.footer" #footer> |
||||
<slot name="footer"></slot> |
||||
</template> |
||||
</ElDialog> |
||||
</template> |
||||
|
||||
<style lang="scss"> |
||||
.#{$elNamespace}-dialog__header { |
||||
margin-right: 0 !important; |
||||
border-bottom: 1px solid var(--tags-view-border-color); |
||||
} |
||||
|
||||
.#{$elNamespace}-dialog__footer { |
||||
border-top: 1px solid var(--tags-view-border-color); |
||||
} |
||||
|
||||
.is-hover { |
||||
&:hover { |
||||
color: var(--el-color-primary) !important; |
||||
} |
||||
} |
||||
|
||||
.dark { |
||||
.#{$elNamespace}-dialog__header { |
||||
border-bottom: 1px solid var(--el-border-color); |
||||
} |
||||
|
||||
.#{$elNamespace}-dialog__footer { |
||||
border-top: 1px solid var(--el-border-color); |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,3 @@ |
||||
import DictTag from './src/DictTag.vue' |
||||
|
||||
export { DictTag } |
@ -0,0 +1,60 @@ |
||||
<script lang="tsx"> |
||||
import { defineComponent, PropType, ref } from 'vue' |
||||
import { isHexColor } from '@/utils/color' |
||||
import { ElTag } from 'element-plus' |
||||
import { DictDataType, getDictOptions } from '@/utils/dict' |
||||
|
||||
export default defineComponent({ |
||||
name: 'DictTag', |
||||
props: { |
||||
type: { |
||||
type: String as PropType<string>, |
||||
required: true |
||||
}, |
||||
value: { |
||||
type: [String, Number, Boolean] as PropType<string | number | boolean>, |
||||
required: true |
||||
} |
||||
}, |
||||
setup(props) { |
||||
const dictData = ref<DictDataType>() |
||||
const getDictObj = (dictType: string, value: string) => { |
||||
const dictOptions = getDictOptions(dictType) |
||||
dictOptions.forEach((dict: DictDataType) => { |
||||
if (dict.value === value) { |
||||
if (dict.colorType + '' === 'primary' || dict.colorType + '' === 'default') { |
||||
dict.colorType = '' |
||||
} |
||||
dictData.value = dict |
||||
} |
||||
}) |
||||
} |
||||
const rederDictTag = () => { |
||||
if (!props.type) { |
||||
return null |
||||
} |
||||
// 解决自定义字典标签值为零时标签不渲染的问题 |
||||
if (props.value === undefined || props.value === null) { |
||||
return null |
||||
} |
||||
getDictObj(props.type, props.value.toString()) |
||||
// 添加标签的文字颜色为白色,解决自定义背景颜色时标签文字看不清的问题 |
||||
return ( |
||||
<ElTag |
||||
style={dictData.value?.cssClass ? 'color: #fff' : ''} |
||||
type={dictData.value?.colorType} |
||||
color={ |
||||
dictData.value?.cssClass && isHexColor(dictData.value?.cssClass) |
||||
? dictData.value?.cssClass |
||||
: '' |
||||
} |
||||
disableTransitions={true} |
||||
> |
||||
{dictData.value?.label} |
||||
</ElTag> |
||||
) |
||||
} |
||||
return () => rederDictTag() |
||||
} |
||||
}) |
||||
</script> |
@ -0,0 +1,32 @@ |
||||
<template> |
||||
<el-alert v-if="getEnable()" type="success" show-icon> |
||||
<template #title> |
||||
<div @click="goToUrl">{{ '【' + title + '】文档地址:' + url }}</div> |
||||
</template> |
||||
</el-alert> |
||||
</template> |
||||
<script setup lang="tsx" name="DocAlert"> |
||||
import { propTypes } from '@/utils/propTypes' |
||||
|
||||
const props = defineProps({ |
||||
title: propTypes.string, |
||||
url: propTypes.string |
||||
}) |
||||
|
||||
/** 跳转 URL 链接 */ |
||||
const goToUrl = () => { |
||||
window.open(props.url) |
||||
} |
||||
|
||||
/** 是否开启 */ |
||||
const getEnable = () => { |
||||
return import.meta.env.VITE_APP_TENANT_ENABLE === 'true' |
||||
} |
||||
</script> |
||||
<style scoped> |
||||
.el-alert--success.is-light { |
||||
border: 1px solid green; |
||||
margin-bottom: 10px; |
||||
cursor: pointer; |
||||
} |
||||
</style> |
@ -0,0 +1,3 @@ |
||||
import Echart from './src/Echart.vue' |
||||
|
||||
export { Echart } |
@ -0,0 +1,114 @@ |
||||
<script lang="ts" name="EChart" setup> |
||||
import type { EChartsOption } from 'echarts' |
||||
import echarts from '@/plugins/echarts' |
||||
import { debounce } from 'lodash-es' |
||||
import 'echarts-wordcloud' |
||||
import { propTypes } from '@/utils/propTypes' |
||||
import { PropType } from 'vue' |
||||
import { useAppStore } from '@/store/modules/app' |
||||
import { isString } from '@/utils/is' |
||||
import { useDesign } from '@/hooks/web/useDesign' |
||||
|
||||
const { getPrefixCls, variables } = useDesign() |
||||
|
||||
const prefixCls = getPrefixCls('echart') |
||||
|
||||
const appStore = useAppStore() |
||||
|
||||
const props = defineProps({ |
||||
options: { |
||||
type: Object as PropType<EChartsOption>, |
||||
required: true |
||||
}, |
||||
width: propTypes.oneOfType([Number, String]).def(''), |
||||
height: propTypes.oneOfType([Number, String]).def('500px') |
||||
}) |
||||
|
||||
const isDark = computed(() => appStore.getIsDark) |
||||
|
||||
const theme = computed(() => { |
||||
const echartTheme: boolean | string = unref(isDark) ? true : 'auto' |
||||
|
||||
return echartTheme |
||||
}) |
||||
|
||||
const options = computed(() => { |
||||
return Object.assign(props.options, { |
||||
darkMode: unref(theme) |
||||
}) |
||||
}) |
||||
|
||||
const elRef = ref<ElRef>() |
||||
|
||||
let echartRef: Nullable<echarts.ECharts> = null |
||||
|
||||
const contentEl = ref<Element>() |
||||
|
||||
const styles = computed(() => { |
||||
const width = isString(props.width) ? props.width : `${props.width}px` |
||||
const height = isString(props.height) ? props.height : `${props.height}px` |
||||
|
||||
return { |
||||
width, |
||||
height |
||||
} |
||||
}) |
||||
|
||||
const initChart = () => { |
||||
if (unref(elRef) && props.options) { |
||||
echartRef = echarts.init(unref(elRef) as HTMLElement) |
||||
echartRef?.setOption(unref(options)) |
||||
} |
||||
} |
||||
|
||||
watch( |
||||
() => options.value, |
||||
(options) => { |
||||
if (echartRef) { |
||||
echartRef?.setOption(options) |
||||
resizeHandler() |
||||
} |
||||
}, |
||||
{ |
||||
deep: true |
||||
} |
||||
) |
||||
|
||||
const resizeHandler = debounce(() => { |
||||
if (echartRef) { |
||||
echartRef.resize() |
||||
} |
||||
}, 100) |
||||
|
||||
const contentResizeHandler = async (e: TransitionEvent) => { |
||||
if (e.propertyName === 'width') { |
||||
resizeHandler() |
||||
} |
||||
} |
||||
|
||||
onMounted(() => { |
||||
initChart() |
||||
|
||||
window.addEventListener('resize', resizeHandler) |
||||
|
||||
contentEl.value = document.getElementsByClassName(`${variables.namespace}-layout-content`)[0] |
||||
unref(contentEl) && |
||||
(unref(contentEl) as Element).addEventListener('transitionend', contentResizeHandler) |
||||
}) |
||||
|
||||
onBeforeUnmount(() => { |
||||
window.removeEventListener('resize', resizeHandler) |
||||
unref(contentEl) && |
||||
(unref(contentEl) as Element).removeEventListener('transitionend', contentResizeHandler) |
||||
}) |
||||
|
||||
onActivated(() => { |
||||
if (echartRef) { |
||||
echartRef.resize() |
||||
} |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div ref="elRef" :class="[$attrs.class, prefixCls]" :style="styles"></div> |
||||
</template> |
@ -0,0 +1,8 @@ |
||||
import Editor from './src/Editor.vue' |
||||
import { IDomEditor } from '@wangeditor/editor' |
||||
|
||||
export interface EditorExpose { |
||||
getEditorRef: () => Promise<IDomEditor> |
||||
} |
||||
|
||||
export { Editor } |
@ -0,0 +1,280 @@ |
||||
<script lang="ts" name="Editor" setup> |
||||
import { PropType } from 'vue' |
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue' |
||||
import { i18nChangeLanguage, IDomEditor, IEditorConfig } from '@wangeditor/editor' |
||||
import { propTypes } from '@/utils/propTypes' |
||||
import { isNumber } from '@/utils/is' |
||||
import { ElMessage } from 'element-plus' |
||||
import { useLocaleStore } from '@/store/modules/locale' |
||||
import { getAccessToken, getTenantId, getAppId } from '@/utils/auth' |
||||
|
||||
type InsertFnType = (url: string, alt: string, href: string) => void |
||||
|
||||
const localeStore = useLocaleStore() |
||||
|
||||
const currentLocale = computed(() => localeStore.getCurrentLocale) |
||||
|
||||
i18nChangeLanguage(unref(currentLocale).lang) |
||||
|
||||
const props = defineProps({ |
||||
editorId: propTypes.string.def('wangeEditor-1'), |
||||
height: propTypes.oneOfType([Number, String]).def('40vh'), |
||||
editorConfig: { |
||||
type: Object as PropType<Partial<IEditorConfig>>, |
||||
default: () => undefined |
||||
}, |
||||
readonly: propTypes.bool.def(false), |
||||
modelValue: propTypes.string.def(''), |
||||
toolbarConfig: { |
||||
type: Object, |
||||
default: () => ({ |
||||
excludeKeys: [ |
||||
'insertVideo', // 网络视频 |
||||
'insertImage', // 网络图片 |
||||
'insertLink', // 链接 |
||||
'codeBlock', // 代码块 |
||||
'headerSelect', // 标题 |
||||
'blockquote', // 引用 |
||||
'fontFamily', // 字体 |
||||
'todo', // 代办 |
||||
'group-indent', // 缩进 |
||||
'emotion', // 表情 |
||||
'undo', // 撤销 |
||||
'redo', // 重做 |
||||
'fullScreen' |
||||
] |
||||
}) |
||||
} |
||||
}) |
||||
|
||||
const emit = defineEmits(['change', 'update:modelValue']) |
||||
|
||||
// 编辑器实例,必须用 shallowRef |
||||
const editorRef = shallowRef<IDomEditor>() |
||||
|
||||
const valueHtml = ref('') |
||||
|
||||
watch( |
||||
() => props.modelValue, |
||||
(val: string) => { |
||||
if (val === unref(valueHtml)) return |
||||
valueHtml.value = val |
||||
}, |
||||
{ |
||||
immediate: true |
||||
} |
||||
) |
||||
|
||||
// 监听 |
||||
watch( |
||||
() => valueHtml.value, |
||||
(val: string) => { |
||||
emit('update:modelValue', val) |
||||
} |
||||
) |
||||
|
||||
const handleCreated = (editor: IDomEditor) => { |
||||
editorRef.value = editor |
||||
} |
||||
|
||||
// 编辑器配置 |
||||
const editorConfig = computed((): IEditorConfig => { |
||||
return Object.assign( |
||||
{ |
||||
placeholder: '请输入内容...', |
||||
readOnly: props.readonly, |
||||
customAlert: (s: string, t: string) => { |
||||
switch (t) { |
||||
case 'success': |
||||
ElMessage.success(s) |
||||
break |
||||
case 'info': |
||||
ElMessage.info(s) |
||||
break |
||||
case 'warning': |
||||
ElMessage.warning(s) |
||||
break |
||||
case 'error': |
||||
ElMessage.error(s) |
||||
break |
||||
default: |
||||
ElMessage.info(s) |
||||
break |
||||
} |
||||
}, |
||||
autoFocus: false, |
||||
scroll: true, |
||||
MENU_CONF: { |
||||
['uploadImage']: { |
||||
server: import.meta.env.VITE_UPLOAD_URL, |
||||
// 单个文件的最大体积限制,默认为 2M |
||||
maxFileSize: 5 * 1024 * 1024, |
||||
// 最多可上传几个文件,默认为 100 |
||||
maxNumberOfFiles: 10, |
||||
// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 [] |
||||
allowedFileTypes: ['image/*'], |
||||
|
||||
// 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。 |
||||
meta: { updateSupport: 0 }, |
||||
// 将 meta 拼接到 url 参数中,默认 false |
||||
metaWithUrl: true, |
||||
|
||||
// 自定义增加 http header |
||||
headers: { |
||||
Accept: '*', |
||||
Authorization: 'Bearer ' + getAccessToken(), |
||||
'tenant-id': getTenantId(), |
||||
'instance-id': getAppId() |
||||
}, |
||||
|
||||
// 跨域是否传递 cookie ,默认为 false |
||||
withCredentials: true, |
||||
|
||||
// 超时时间,默认为 10 秒 |
||||
timeout: 5 * 1000, // 5 秒 |
||||
|
||||
// form-data fieldName,后端接口参数名称,默认值wangeditor-uploaded-image |
||||
fieldName: 'file', |
||||
|
||||
// 上传之前触发 |
||||
onBeforeUpload(file: File) { |
||||
console.log(file) |
||||
return file |
||||
}, |
||||
// 上传进度的回调函数 |
||||
onProgress(progress: number) { |
||||
// progress 是 0-100 的数字 |
||||
console.log('progress', progress) |
||||
}, |
||||
onSuccess(file: File, res: any) { |
||||
console.log('onSuccess', file, res) |
||||
}, |
||||
onFailed(file: File, res: any) { |
||||
alert(res.message) |
||||
console.log('onFailed', file, res) |
||||
}, |
||||
onError(file: File, err: any, res: any) { |
||||
alert(err.message) |
||||
console.error('onError', file, err, res) |
||||
}, |
||||
// 自定义插入图片 |
||||
customInsert(res: any, insertFn: InsertFnType) { |
||||
insertFn(res.data, 'image', res.data) |
||||
} |
||||
}, |
||||
['uploadVideo']: { |
||||
server: import.meta.env.VITE_UPLOAD_URL, |
||||
// 单个文件的最大体积限制,默认为 2M |
||||
maxFileSize: 100 * 1024 * 1024, |
||||
// 最多可上传几个文件,默认为 100 |
||||
maxNumberOfFiles: 10, |
||||
// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 [] |
||||
allowedFileTypes: ['video/*'], |
||||
|
||||
// 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。 |
||||
meta: { updateSupport: 0 }, |
||||
// 将 meta 拼接到 url 参数中,默认 false |
||||
metaWithUrl: true, |
||||
|
||||
// 自定义增加 http header |
||||
headers: { |
||||
Accept: '*', |
||||
Authorization: 'Bearer ' + getAccessToken(), |
||||
'tenant-id': getTenantId(), |
||||
'instance-id': getAppId() |
||||
}, |
||||
|
||||
// 跨域是否传递 cookie ,默认为 false |
||||
withCredentials: true, |
||||
|
||||
// 超时时间,默认为 10 秒 |
||||
timeout: 10 * 1000, // 5 秒 |
||||
|
||||
// form-data fieldName,后端接口参数名称,默认值wangeditor-uploaded-image |
||||
fieldName: 'file', |
||||
|
||||
// 上传之前触发 |
||||
onBeforeUpload(file: File) { |
||||
console.log(file) |
||||
return file |
||||
}, |
||||
// 上传进度的回调函数 |
||||
onProgress(progress: number) { |
||||
// progress 是 0-100 的数字 |
||||
console.log('progress', progress) |
||||
}, |
||||
onSuccess(file: File, res: any) { |
||||
console.log('onSuccess', file, res) |
||||
}, |
||||
onFailed(file: File, res: any) { |
||||
alert(res.message) |
||||
console.log('onFailed', file, res) |
||||
}, |
||||
onError(file: File, err: any, res: any) { |
||||
alert(err.message) |
||||
console.error('onError', file, err, res) |
||||
}, |
||||
// 自定义插入图片 |
||||
customInsert(res: any, insertFn: InsertFnType) { |
||||
insertFn(res.data, 'video', res.data) |
||||
} |
||||
} |
||||
}, |
||||
uploadImgShowBase64: true |
||||
}, |
||||
props.editorConfig || {} |
||||
) |
||||
}) |
||||
|
||||
const editorStyle = computed(() => { |
||||
return { |
||||
height: isNumber(props.height) ? `${props.height}px` : props.height |
||||
} |
||||
}) |
||||
|
||||
// 回调函数 |
||||
const handleChange = (editor: IDomEditor) => { |
||||
emit('change', editor) |
||||
} |
||||
|
||||
// 组件销毁时,及时销毁编辑器 |
||||
onBeforeUnmount(() => { |
||||
const editor = unref(editorRef.value) |
||||
if (editor === null) return |
||||
|
||||
// 销毁,并移除 editor |
||||
editor?.destroy() |
||||
}) |
||||
|
||||
const getEditorRef = async (): Promise<IDomEditor> => { |
||||
await nextTick() |
||||
return unref(editorRef.value) as IDomEditor |
||||
} |
||||
|
||||
defineExpose({ |
||||
getEditorRef |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="border-1 border-solid border-[var(--tags-view-border-color)] z-99"> |
||||
<!-- 工具栏 --> |
||||
<Toolbar |
||||
:editor="editorRef" |
||||
:editorId="editorId" |
||||
:defaultConfig="toolbarConfig" |
||||
class="border-bottom-1 border-solid border-[var(--tags-view-border-color)]" |
||||
/> |
||||
<!-- 编辑器 --> |
||||
<Editor |
||||
v-model="valueHtml" |
||||
:defaultConfig="editorConfig" |
||||
:editorId="editorId" |
||||
:style="editorStyle" |
||||
@on-change="handleChange" |
||||
@on-created="handleCreated" |
||||
/> |
||||
</div> |
||||
</template> |
||||
|
||||
<style src="@wangeditor/editor/dist/css/style.css"></style> |
@ -0,0 +1,68 @@ |
||||
|
||||
**「工具栏key」** |
||||
[ |
||||
"headerSelect",// 标题 |
||||
"blockquote", // 引用 |
||||
"bold", // 加粗 |
||||
"underline", // 下划线 |
||||
"italic", // 斜体 |
||||
// 删除线、清除格式等 |
||||
"group-more-style", |
||||
{ |
||||
key: "group-more-style", |
||||
title: "更多", |
||||
iconSvg: |
||||
'<svg viewBox="0 0 1024 1024"><path d="M204.8 505.6…0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path></svg>', |
||||
menuKeys: Array(5) |
||||
}, |
||||
"color", // 文字颜色 |
||||
"bgColor", // 背景色 |
||||
"fontSize", // 字号 |
||||
"fontFamily", // 字体 |
||||
"lineHeight", // 行高 |
||||
"bulletedList", // 无序列表 |
||||
"numberedList", // 有序列表 |
||||
"todo", // 代办 |
||||
// 对齐 |
||||
"group-justify", |
||||
{ |
||||
key: "group-justify", |
||||
title: "对齐", |
||||
iconSvg: |
||||
'<svg viewBox="0 0 1024 1024"><path d="M768 793.6v1…72.8 102.4v102.4H51.2V102.4h921.6z"></path></svg>', |
||||
menuKeys: Array(4) |
||||
}, |
||||
// 缩进 |
||||
"group-indent", |
||||
{ |
||||
key: "group-indent", |
||||
title: "缩进", |
||||
iconSvg: |
||||
'<svg viewBox="0 0 1024 1024"><path d="M0 64h1024v1…32h1024v128H0z m0-128V320l256 192z"></path></svg>', |
||||
menuKeys: Array(2) |
||||
}, |
||||
"emotion",// 表情 |
||||
"insertLink",// 插入链接 |
||||
"group-image",// 上传图片 |
||||
{ |
||||
key: "group-image", |
||||
title: "图片", |
||||
iconSvg: |
||||
'<svg viewBox="0 0 1024 1024"><path d="M959.877 128…l224.01-384 256 320h64l224.01-192z"></path></svg>', |
||||
menuKeys: Array(2) |
||||
}, |
||||
"group-video",// 上传视频 |
||||
{ |
||||
key: "group-video", |
||||
title: "视频", |
||||
iconSvg: |
||||
'<svg viewBox="0 0 1024 1024"><path d="M981.184 160….904zM384 704V320l320 192-320 192z"></path></svg>', |
||||
menuKeys: Array(2) |
||||
}, |
||||
"insertTable",// 插入表格 |
||||
"codeBlock", // 代码块 |
||||
"divider", // 分割线 |
||||
"undo", // 撤销 |
||||
"redo", // 重做 |
||||
"fullScreen" // 全屏 |
||||
] |
@ -0,0 +1,3 @@ |
||||
import Error from './src/Error.vue' |
||||
|
||||
export { Error } |
@ -0,0 +1,56 @@ |
||||
<script lang="ts" name="Error" setup> |
||||
import pageError from '@/assets/svgs/404.svg' |
||||
import networkError from '@/assets/svgs/500.svg' |
||||
import noPermission from '@/assets/svgs/403.svg' |
||||
import { propTypes } from '@/utils/propTypes' |
||||
|
||||
interface ErrorMap { |
||||
url: string |
||||
message: string |
||||
buttonText: string |
||||
} |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
const errorMap: { |
||||
[key: string]: ErrorMap |
||||
} = { |
||||
'404': { |
||||
url: pageError, |
||||
message: t('error.pageError'), |
||||
buttonText: t('error.returnToHome') |
||||
}, |
||||
'500': { |
||||
url: networkError, |
||||
message: t('error.networkError'), |
||||
buttonText: t('error.returnToHome') |
||||
}, |
||||
'403': { |
||||
url: noPermission, |
||||
message: t('error.noPermission'), |
||||
buttonText: t('error.returnToHome') |
||||
} |
||||
} |
||||
|
||||
const props = defineProps({ |
||||
type: propTypes.string.validate((v: string) => ['404', '500', '403'].includes(v)).def('404') |
||||
}) |
||||
|
||||
const emit = defineEmits(['errorClick']) |
||||
|
||||
const btnClick = () => { |
||||
emit('errorClick', props.type) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="flex justify-center"> |
||||
<div class="text-center"> |
||||
<img :src="errorMap[type].url" alt="" width="350" /> |
||||
<div class="text-14px text-[var(--el-color-info)]">{{ errorMap[type].message }}</div> |
||||
<div class="mt-20px"> |
||||
<ElButton type="primary" @click="btnClick">{{ errorMap[type].buttonText }}</ElButton> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
@ -0,0 +1,15 @@ |
||||
import Form from './src/Form.vue' |
||||
import { ElForm } from 'element-plus' |
||||
import { FormSchema, FormSetPropsType } from '@/types/form' |
||||
|
||||
export interface FormExpose { |
||||
setValues: (data: Recordable) => void |
||||
setProps: (props: Recordable) => void |
||||
delSchema: (field: string) => void |
||||
addSchema: (formSchema: FormSchema, index?: number) => void |
||||
setSchema: (schemaProps: FormSetPropsType[]) => void |
||||
formModel: Recordable |
||||
getElFormRef: () => ComponentRef<typeof ElForm> |
||||
} |
||||
|
||||
export { Form } |
@ -0,0 +1,314 @@ |
||||
<script lang="tsx"> |
||||
import { PropType, defineComponent, ref, computed, unref, watch, onMounted } from 'vue' |
||||
import { ElForm, ElFormItem, ElRow, ElCol, ElTooltip } from 'element-plus' |
||||
import { componentMap } from './componentMap' |
||||
import { propTypes } from '@/utils/propTypes' |
||||
import { getSlot } from '@/utils/tsxHelper' |
||||
import { |
||||
setTextPlaceholder, |
||||
setGridProp, |
||||
setComponentProps, |
||||
setItemComponentSlots, |
||||
initModel, |
||||
setFormItemSlots |
||||
} from './helper' |
||||
import { useRenderSelect } from './components/useRenderSelect' |
||||
import { useRenderRadio } from './components/useRenderRadio' |
||||
import { useRenderCheckbox } from './components/useRenderCheckbox' |
||||
import { useDesign } from '@/hooks/web/useDesign' |
||||
import { findIndex } from '@/utils' |
||||
import { set } from 'lodash-es' |
||||
import { FormProps } from './types' |
||||
import { Icon } from '@/components/Icon' |
||||
import { FormSchema, FormSetPropsType } from '@/types/form' |
||||
|
||||
const { getPrefixCls } = useDesign() |
||||
|
||||
const prefixCls = getPrefixCls('form') |
||||
|
||||
export default defineComponent({ |
||||
name: 'Form', |
||||
props: { |
||||
// 生成Form的布局结构数组 |
||||
schema: { |
||||
type: Array as PropType<FormSchema[]>, |
||||
default: () => [] |
||||
}, |
||||
// 是否需要栅格布局 |
||||
// update by 芋艿:将 true 改成 false,因为项目更常用这种方式 |
||||
isCol: propTypes.bool.def(false), |
||||
// 是否搜索 |
||||
isSearch: propTypes.bool.def(false), |
||||
// 表单数据对象 |
||||
model: { |
||||
type: Object as PropType<Recordable>, |
||||
default: () => ({}) |
||||
}, |
||||
// 是否自动设置placeholder |
||||
autoSetPlaceholder: propTypes.bool.def(true), |
||||
// 是否自定义内容 |
||||
isCustom: propTypes.bool.def(false), |
||||
// 表单label宽度 |
||||
labelWidth: propTypes.oneOfType([String, Number]).def('auto'), |
||||
// 是否 loading 数据中 add by 芋艿 |
||||
vLoading: propTypes.bool.def(false), |
||||
inlineBlock: propTypes.bool.def(false) |
||||
}, |
||||
emits: ['register'], |
||||
setup(props, { slots, expose, emit }) { |
||||
// element form 实例 |
||||
const elFormRef = ref<ComponentRef<typeof ElForm>>() |
||||
|
||||
// useForm传入的props |
||||
const outsideProps = ref<FormProps>({}) |
||||
|
||||
const mergeProps = ref<FormProps>({}) |
||||
|
||||
const getProps = computed(() => { |
||||
const propsObj = { ...props } |
||||
Object.assign(propsObj, unref(mergeProps)) |
||||
return propsObj |
||||
}) |
||||
|
||||
// 表单数据 |
||||
const formModel = ref<Recordable>({}) |
||||
|
||||
onMounted(() => { |
||||
emit('register', unref(elFormRef)?.$parent, unref(elFormRef)) |
||||
}) |
||||
|
||||
// 对表单赋值 |
||||
const setValues = (data: Recordable = {}) => { |
||||
formModel.value = Object.assign(unref(formModel), data) |
||||
} |
||||
|
||||
const setProps = (props: FormProps = {}) => { |
||||
mergeProps.value = Object.assign(unref(mergeProps), props) |
||||
outsideProps.value = props |
||||
} |
||||
|
||||
const delSchema = (field: string) => { |
||||
const { schema } = unref(getProps) |
||||
|
||||
const index = findIndex(schema, (v: FormSchema) => v.field === field) |
||||
if (index > -1) { |
||||
schema.splice(index, 1) |
||||
} |
||||
} |
||||
|
||||
const addSchema = (formSchema: FormSchema, index?: number) => { |
||||
const { schema } = unref(getProps) |
||||
if (index !== void 0) { |
||||
schema.splice(index, 0, formSchema) |
||||
return |
||||
} |
||||
schema.push(formSchema) |
||||
} |
||||
|
||||
const setSchema = (schemaProps: FormSetPropsType[]) => { |
||||
const { schema } = unref(getProps) |
||||
for (const v of schema) { |
||||
for (const item of schemaProps) { |
||||
if (v.field === item.field) { |
||||
set(v, item.path, item.value) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
const getElFormRef = (): ComponentRef<typeof ElForm> => { |
||||
return unref(elFormRef) as ComponentRef<typeof ElForm> |
||||
} |
||||
|
||||
expose({ |
||||
setValues, |
||||
formModel, |
||||
setProps, |
||||
delSchema, |
||||
addSchema, |
||||
setSchema, |
||||
getElFormRef |
||||
}) |
||||
|
||||
// 监听表单结构化数组,重新生成formModel |
||||
watch( |
||||
() => unref(getProps).schema, |
||||
(schema = []) => { |
||||
formModel.value = initModel(schema, unref(formModel)) |
||||
}, |
||||
{ |
||||
immediate: true, |
||||
deep: true |
||||
} |
||||
) |
||||
|
||||
// 渲染包裹标签,是否使用栅格布局 |
||||
const renderWrap = () => { |
||||
const { isCol } = unref(getProps) |
||||
const content = isCol ? ( |
||||
<ElRow gutter={20}>{renderFormItemWrap()}</ElRow> |
||||
) : ( |
||||
renderFormItemWrap() |
||||
) |
||||
return content |
||||
} |
||||
|
||||
// 是否要渲染el-col |
||||
const renderFormItemWrap = () => { |
||||
// hidden属性表示隐藏,不做渲染 |
||||
const { schema = [], isCol } = unref(getProps) |
||||
|
||||
return schema |
||||
.filter((v) => !v.hidden) |
||||
.map((item) => { |
||||
// 如果是 Divider 组件,需要自己占用一行 |
||||
const isDivider = item.component === 'Divider' |
||||
const Com = componentMap['Divider'] as ReturnType<typeof defineComponent> |
||||
return isDivider ? ( |
||||
<Com {...{ contentPosition: 'left', ...item.componentProps }}>{item?.label}</Com> |
||||
) : isCol ? ( |
||||
// 如果需要栅格,需要包裹 ElCol |
||||
<ElCol {...setGridProp(item.colProps)}>{renderFormItem(item)}</ElCol> |
||||
) : ( |
||||
renderFormItem(item) |
||||
) |
||||
}) |
||||
} |
||||
|
||||
// 渲染formItem |
||||
const renderFormItem = (item: FormSchema) => { |
||||
// 单独给只有options属性的组件做判断 |
||||
const notRenderOptions = ['SelectV2', 'Cascader', 'Transfer'] |
||||
const slotsMap: Recordable = { |
||||
...setItemComponentSlots(slots, item?.componentProps?.slots, item.field) |
||||
} |
||||
if (item?.component !== 'SelectV2' && item?.component !== 'Cascader' && item?.options) { |
||||
slotsMap.default = () => renderOptions(item) |
||||
} |
||||
|
||||
const formItemSlots: Recordable = setFormItemSlots(slots, item.field) |
||||
// 如果有 labelMessage,自动使用插槽渲染 |
||||
if (item?.labelMessage) { |
||||
formItemSlots.label = () => { |
||||
return ( |
||||
<> |
||||
<span>{item.label}</span> |
||||
<ElTooltip placement="right" raw-content> |
||||
{{ |
||||
content: () => <span v-html={item.labelMessage}></span>, |
||||
default: () => ( |
||||
<Icon |
||||
icon="ep:warning" |
||||
size={16} |
||||
color="var(--el-color-primary)" |
||||
class="ml-2px relative top-1px" |
||||
></Icon> |
||||
) |
||||
}} |
||||
</ElTooltip> |
||||
</> |
||||
) |
||||
} |
||||
} |
||||
const { isSearch } = unref(getProps) |
||||
return ( |
||||
<ElFormItem |
||||
class={isSearch ? 'search-form-item' : 'crud-form-item'} |
||||
{...(item.formItemProps || {})} |
||||
prop={item.field} |
||||
label={item.label || ''} |
||||
> |
||||
{{ |
||||
...formItemSlots, |
||||
default: () => { |
||||
const Com = componentMap[item.component as string] as ReturnType< |
||||
typeof defineComponent |
||||
> |
||||
const baseSty = isSearch ? '' : 'width: 100%;' |
||||
const { autoSetPlaceholder } = unref(getProps) |
||||
return slots[item.field] ? ( |
||||
getSlot(slots, item.field, formModel.value) |
||||
) : ( |
||||
<Com |
||||
vModel={formModel.value[item.field]} |
||||
{...(autoSetPlaceholder && setTextPlaceholder(item))} |
||||
{...setComponentProps(item)} |
||||
filterable |
||||
format={item.component == 'DatePicker' ? 'YYYY-MM-DD' : null} |
||||
value-format={item.component == 'DatePicker' ? 'YYYY-MM-DD' : null} |
||||
style={baseSty + item.componentProps?.style} |
||||
// eslint-disable-next-line prettier/prettier |
||||
{...(notRenderOptions.includes(item?.component as string) && item?.componentProps?.options |
||||
? { options: item?.componentProps?.options || [] } |
||||
: {})} |
||||
> |
||||
{{ ...slotsMap }} |
||||
</Com> |
||||
) |
||||
} |
||||
}} |
||||
</ElFormItem> |
||||
) |
||||
} |
||||
|
||||
// 渲染options |
||||
const renderOptions = (item: FormSchema) => { |
||||
switch (item.component) { |
||||
case 'Select': |
||||
case 'SelectV2': |
||||
const { renderSelectOptions } = useRenderSelect(slots) |
||||
return renderSelectOptions(item) |
||||
case 'Radio': |
||||
case 'RadioButton': |
||||
const { renderRadioOptions } = useRenderRadio() |
||||
return renderRadioOptions(item) |
||||
case 'Checkbox': |
||||
case 'CheckboxButton': |
||||
const { renderCheckboxOptions } = useRenderCheckbox() |
||||
return renderCheckboxOptions(item) |
||||
default: |
||||
break |
||||
} |
||||
} |
||||
|
||||
// 过滤传入Form组件的属性 |
||||
const getFormBindValue = () => { |
||||
// 避免在标签上出现多余的属性 |
||||
const delKeys = ['schema', 'isCol', 'autoSetPlaceholder', 'isCustom', 'model'] |
||||
const props = { ...unref(getProps) } |
||||
for (const key in props) { |
||||
if (delKeys.indexOf(key) !== -1) { |
||||
delete props[key] |
||||
} |
||||
} |
||||
return props |
||||
} |
||||
|
||||
return () => ( |
||||
<ElForm |
||||
ref={elFormRef} |
||||
{...getFormBindValue()} |
||||
model={props.isCustom ? props.model : formModel} |
||||
class={prefixCls} |
||||
v-loading={props.vLoading} |
||||
style={props.inlineBlock ? 'display: inline' : ''} |
||||
> |
||||
{{ |
||||
// 如果需要自定义,就什么都不渲染,而是提供默认插槽 |
||||
default: () => { |
||||
const { isCustom } = unref(getProps) |
||||
return isCustom ? getSlot(slots, 'default') : renderWrap() |
||||
} |
||||
}} |
||||
</ElForm> |
||||
) |
||||
} |
||||
}) |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.#{$elNamespace}-form.#{$namespace}-form .#{$elNamespace}-row { |
||||
margin-right: 0 !important; |
||||
margin-left: 0 !important; |
||||
} |
||||
</style> |
@ -0,0 +1,55 @@ |
||||
import type { Component } from 'vue' |
||||
import { |
||||
ElCascader, |
||||
ElCheckboxGroup, |
||||
ElColorPicker, |
||||
ElDatePicker, |
||||
ElInput, |
||||
ElInputNumber, |
||||
ElRadioGroup, |
||||
ElRate, |
||||
ElSelect, |
||||
ElSelectV2, |
||||
ElTreeSelect, |
||||
ElSlider, |
||||
ElSwitch, |
||||
ElTimePicker, |
||||
ElTimeSelect, |
||||
ElTransfer, |
||||
ElAutocomplete, |
||||
ElDivider |
||||
} from 'element-plus' |
||||
import { InputPassword } from '@/components/InputPassword' |
||||
import { Editor } from '@/components/Editor' |
||||
import { UploadImg, UploadImgs, UploadFile } from '@/components/UploadFile' |
||||
import { ComponentName } from '@/types/components' |
||||
|
||||
const componentMap: Recordable<Component, ComponentName> = { |
||||
Radio: ElRadioGroup, |
||||
Checkbox: ElCheckboxGroup, |
||||
CheckboxButton: ElCheckboxGroup, |
||||
Input: ElInput, |
||||
Autocomplete: ElAutocomplete, |
||||
InputNumber: ElInputNumber, |
||||
Select: ElSelect, |
||||
Cascader: ElCascader, |
||||
Switch: ElSwitch, |
||||
Slider: ElSlider, |
||||
TimePicker: ElTimePicker, |
||||
DatePicker: ElDatePicker, |
||||
Rate: ElRate, |
||||
ColorPicker: ElColorPicker, |
||||
Transfer: ElTransfer, |
||||
Divider: ElDivider, |
||||
TimeSelect: ElTimeSelect, |
||||
SelectV2: ElSelectV2, |
||||
TreeSelect: ElTreeSelect, |
||||
RadioButton: ElRadioGroup, |
||||
InputPassword: InputPassword, |
||||
Editor: Editor, |
||||
UploadImg: UploadImg, |
||||
UploadImgs: UploadImgs, |
||||
UploadFile: UploadFile |
||||
} |
||||
|
||||
export { componentMap } |
@ -0,0 +1,26 @@ |
||||
import { FormSchema } from '@/types/form' |
||||
import { ElCheckbox, ElCheckboxButton } from 'element-plus' |
||||
import { defineComponent } from 'vue' |
||||
|
||||
export const useRenderCheckbox = () => { |
||||
const renderCheckboxOptions = (item: FormSchema) => { |
||||
// 如果有别名,就取别名
|
||||
const labelAlias = item?.componentProps?.optionsAlias?.labelField || 'id' |
||||
const valueAlias = item?.componentProps?.optionsAlias?.valueField || 'name' |
||||
const Com = (item.component === 'Checkbox' ? ElCheckbox : ElCheckboxButton) as ReturnType< |
||||
typeof defineComponent |
||||
> |
||||
return item?.options?.map((option) => { |
||||
const { ...other } = option |
||||
return ( |
||||
<Com {...other} label={option[valueAlias || 'value']}> |
||||
{option[labelAlias || 'label']} |
||||
</Com> |
||||
) |
||||
}) |
||||
} |
||||
|
||||
return { |
||||
renderCheckboxOptions |
||||
} |
||||
} |
@ -0,0 +1,26 @@ |
||||
import { FormSchema } from '@/types/form' |
||||
import { ElRadio, ElRadioButton } from 'element-plus' |
||||
import { defineComponent } from 'vue' |
||||
|
||||
export const useRenderRadio = () => { |
||||
const renderRadioOptions = (item: FormSchema) => { |
||||
// 如果有别名,就取别名
|
||||
const labelAlias = item?.componentProps?.optionsAlias?.labelField || 'id' |
||||
const valueAlias = item?.componentProps?.optionsAlias?.valueField || 'name' |
||||
const Com = (item.component === 'Radio' ? ElRadio : ElRadioButton) as ReturnType< |
||||
typeof defineComponent |
||||
> |
||||
return item?.options?.map((option) => { |
||||
const { ...other } = option |
||||
return ( |
||||
<Com {...other} label={option[valueAlias || 'value']}> |
||||
{option[labelAlias || 'label']} |
||||
</Com> |
||||
) |
||||
}) |
||||
} |
||||
|
||||
return { |
||||
renderRadioOptions |
||||
} |
||||
} |
@ -0,0 +1,57 @@ |
||||
import { FormSchema } from '@/types/form' |
||||
import { ComponentOptions } from '@/types/components' |
||||
import { ElOption, ElOptionGroup } from 'element-plus' |
||||
import { getSlot } from '@/utils/tsxHelper' |
||||
import { Slots } from 'vue' |
||||
|
||||
export const useRenderSelect = (slots: Slots) => { |
||||
// 渲染 select options
|
||||
const renderSelectOptions = (item: FormSchema) => { |
||||
// 如果有别名,就取别名
|
||||
const labelAlias = item?.componentProps?.optionsAlias?.labelField |
||||
return item?.options?.map((option) => { |
||||
if (option?.length) { |
||||
return ( |
||||
<ElOptionGroup label={option[labelAlias || 'label']}> |
||||
{() => { |
||||
return option?.map((v) => { |
||||
return renderSelectOptionItem(item, v) |
||||
}) |
||||
}} |
||||
</ElOptionGroup> |
||||
) |
||||
} else { |
||||
return renderSelectOptionItem(item, option) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// 渲染 select option item
|
||||
const renderSelectOptionItem = (item: FormSchema, option: ComponentOptions) => { |
||||
// 如果有别名,就取别名
|
||||
const labelAlias = item?.componentProps?.optionsAlias?.labelField || 'id' |
||||
const valueAlias = item?.componentProps?.optionsAlias?.valueField || 'name' |
||||
|
||||
const { label, value, ...other } = option |
||||
|
||||
return ( |
||||
<ElOption |
||||
{...other} |
||||
label={labelAlias ? option[labelAlias] : label} |
||||
value={valueAlias ? option[valueAlias] : value} |
||||
> |
||||
{{ |
||||
default: () => |
||||
// option 插槽名规则,{field}-option
|
||||
item?.componentProps?.optionsSlot |
||||
? getSlot(slots, `${item.field}-option`, { item: option }) |
||||
: undefined |
||||
}} |
||||
</ElOption> |
||||
) |
||||
} |
||||
|
||||
return { |
||||
renderSelectOptions |
||||
} |
||||
} |