sv
12
.editorconfig
Normal file
@@ -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 # 关闭末尾空格修剪
|
||||
20
.env
Normal file
@@ -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
|
||||
24
.env.base
Normal file
@@ -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=/ss-api
|
||||
|
||||
# 接口地址
|
||||
VITE_API_URL=/admin-api
|
||||
|
||||
# 打包路径
|
||||
VITE_BASE_PATH=/ss/
|
||||
31
.env.dev
Normal file
@@ -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=/ss-api
|
||||
|
||||
# 接口地址
|
||||
VITE_API_URL=/admin-api
|
||||
|
||||
# 打包路径
|
||||
VITE_BASE_PATH=/ss/
|
||||
|
||||
# 是否删除debugger
|
||||
VITE_DROP_DEBUGGER=false
|
||||
|
||||
# 是否删除console.log
|
||||
VITE_DROP_CONSOLE=false
|
||||
|
||||
# 是否sourcemap
|
||||
VITE_SOURCEMAP=true
|
||||
|
||||
# 输出路径
|
||||
VITE_OUT_DIR=dist-dev
|
||||
34
.env.front
Normal file
@@ -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=/ss-api
|
||||
|
||||
# 接口地址
|
||||
VITE_API_URL=/admin-api
|
||||
|
||||
# 打包路径
|
||||
VITE_BASE_PATH=/ss/
|
||||
|
||||
# 项目本地运行端口号, 与.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
|
||||
31
.env.pro
Normal file
@@ -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=/ss-api
|
||||
|
||||
# 接口地址
|
||||
VITE_API_URL=/admin-api
|
||||
|
||||
# 是否删除debugger
|
||||
VITE_DROP_DEBUGGER=true
|
||||
|
||||
# 是否删除console.log
|
||||
VITE_DROP_CONSOLE=true
|
||||
|
||||
# 是否sourcemap
|
||||
VITE_SOURCEMAP=false
|
||||
|
||||
# 打包路径
|
||||
VITE_BASE_PATH=/ss/
|
||||
|
||||
# 输出路径
|
||||
VITE_OUT_DIR=dist-pro
|
||||
31
.env.stage
Normal file
@@ -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=/ss-api
|
||||
|
||||
# 接口地址
|
||||
VITE_API_URL=/admin-api
|
||||
|
||||
# 是否删除debugger
|
||||
VITE_DROP_DEBUGGER=true
|
||||
|
||||
# 是否删除console.log
|
||||
VITE_DROP_CONSOLE=true
|
||||
|
||||
# 是否sourcemap
|
||||
VITE_SOURCEMAP=false
|
||||
|
||||
# 打包路径
|
||||
VITE_BASE_PATH='/ss/'
|
||||
|
||||
# 输出路径
|
||||
VITE_OUT_DIR=dist-stage
|
||||
31
.env.static
Normal file
@@ -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=/ss-api
|
||||
|
||||
# 接口地址
|
||||
VITE_API_URL=/admin-api
|
||||
|
||||
# 是否删除debugger
|
||||
VITE_DROP_DEBUGGER=true
|
||||
|
||||
# 是否删除console.log
|
||||
VITE_DROP_CONSOLE=true
|
||||
|
||||
# 是否sourcemap
|
||||
VITE_SOURCEMAP=false
|
||||
|
||||
# 打包路径
|
||||
VITE_BASE_PATH=/ss/
|
||||
|
||||
# 输出路径
|
||||
VITE_OUT_DIR=dist-dev
|
||||
31
.env.test
Normal file
@@ -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=/ss-api
|
||||
|
||||
# 接口地址
|
||||
VITE_API_URL=/admin-api
|
||||
|
||||
# 是否删除debugger
|
||||
VITE_DROP_DEBUGGER=false
|
||||
|
||||
# 是否删除console.log
|
||||
VITE_DROP_CONSOLE=false
|
||||
|
||||
# 是否sourcemap
|
||||
VITE_SOURCEMAP=true
|
||||
|
||||
# 打包路径
|
||||
VITE_BASE_PATH=/ss/
|
||||
|
||||
# 输出路径
|
||||
VITE_OUT_DIR=dist-test
|
||||
8
.eslintignore
Normal file
@@ -0,0 +1,8 @@
|
||||
/build/
|
||||
/config/
|
||||
/dist/
|
||||
/*.js
|
||||
/test/unit/coverage/
|
||||
/node_modules/*
|
||||
/dist*
|
||||
/src/main.ts
|
||||
259
.eslintrc-auto-import.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
69
.eslintrc.js
Normal file
@@ -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'
|
||||
}
|
||||
})
|
||||
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
/dist*
|
||||
*-lock.*
|
||||
pnpm-debug
|
||||
auto-*.d.ts
|
||||
.idea
|
||||
.history
|
||||
11
.prettierignore
Normal file
@@ -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
|
||||
6
.stylelintignore
Normal file
@@ -0,0 +1,6 @@
|
||||
/dist/*
|
||||
/public/*
|
||||
public/*
|
||||
/dist*
|
||||
/src/types/env.d.ts
|
||||
/docs/**/*
|
||||
21
LICENSE
Normal file
@@ -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.
|
||||
109
build/vite/index.ts
Normal file
@@ -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}`
|
||||
})
|
||||
]
|
||||
}
|
||||
111
build/vite/optimize.ts
Normal file
@@ -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 }
|
||||
143
index.html
Normal file
@@ -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>
|
||||
142
package.json
Normal file
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"name": "ss-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"
|
||||
}
|
||||
5
postcss.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
||||
22
prettier.config.js
Normal file
@@ -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
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
public/logo.gif
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
54
src/App.vue
Normal file
@@ -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>
|
||||
48
src/api/infra/config/index.ts
Normal file
@@ -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 })
|
||||
}
|
||||
45
src/api/infra/file/index.ts
Normal file
@@ -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 })
|
||||
}
|
||||
73
src/api/login/index.ts
Normal file
@@ -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/platform/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: any) => {
|
||||
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
|
||||
})
|
||||
}
|
||||
41
src/api/login/oauth2/index.ts
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
28
src/api/login/types.ts
Normal file
@@ -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
|
||||
}
|
||||
5
src/api/system/app/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export const getSimpleAppList = async () => {
|
||||
return await request.get({ url: '/admin-api/system/serviceInstance/simple-list' })
|
||||
}
|
||||
43
src/api/system/dept/index.ts
Normal file
@@ -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 })
|
||||
}
|
||||
54
src/api/system/dict/dict.data.ts
Normal file
@@ -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 } })
|
||||
}
|
||||
44
src/api/system/dict/dict.type.ts
Normal file
@@ -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 })
|
||||
}
|
||||
54
src/api/system/menu/index.ts
Normal file
@@ -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 })
|
||||
}
|
||||
53
src/api/system/notify/message/index.ts
Normal file
@@ -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 } })
|
||||
}
|
||||
49
src/api/system/notify/template/index.ts
Normal file
@@ -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 })
|
||||
}
|
||||
42
src/api/system/permission/index.ts
Normal file
@@ -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 })
|
||||
}
|
||||
53
src/api/system/role/index.ts
Normal file
@@ -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 })
|
||||
}
|
||||
16
src/api/system/set/index.js
Normal file
@@ -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 })
|
||||
}
|
||||
76
src/api/system/user/index.ts
Normal file
@@ -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' })
|
||||
}
|
||||
77
src/api/system/user/profile.ts
Normal file
@@ -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 })
|
||||
}
|
||||
31
src/api/system/user/socialUser.ts
Normal file
@@ -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
|
||||
})
|
||||
}
|
||||
BIN
src/assets/fonts/DISPLAY FREE TFB.ttf
Normal file
BIN
src/assets/fonts/字魂74号-飞墨手书_爱给网_aigei_com.ttf
Normal file
BIN
src/assets/imgs/avatar.gif
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
src/assets/imgs/avatar.jpg
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
src/assets/imgs/login2.gif
Normal file
|
After Width: | Height: | Size: 309 KiB |
BIN
src/assets/imgs/profile.jpg
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
src/assets/imgs/shisong.jpg
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
src/assets/imgs/wechat.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
1
src/assets/svgs/403.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
1
src/assets/svgs/404.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
1
src/assets/svgs/500.svg
Normal file
|
After Width: | Height: | Size: 19 KiB |
1
src/assets/svgs/icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.147.062a13 13 0 014.94.945c1.55.63 2.907 1.526 4.069 2.688a13.148 13.148 0 012.761 4.069c.678 1.55 1.017 3.245 1.017 5.086v102.3c0 3.681-1.187 6.733-3.56 9.155-2.373 2.422-5.352 3.633-8.937 3.633H12.992c-3.875 0-7-1.26-9.373-3.779-2.373-2.518-3.56-5.667-3.56-9.445V12.704c0-3.39 1.163-6.345 3.488-8.863C5.872 1.32 8.972.062 12.847.062h102.3zM81.434 109.047c1.744 0 3.003-.412 3.778-1.235.775-.824 1.163-1.914 1.163-3.27 0-1.26-.388-2.325-1.163-3.197-.775-.872-2.034-1.307-3.778-1.307H72.57c.097-.194.145-.485.145-.872V27.09h9.01c1.743 0 2.954-.436 3.633-1.308.678-.872 1.017-1.938 1.017-3.197 0-1.26-.34-2.325-1.017-3.197-.679-.872-1.89-1.308-3.633-1.308H46.268c-1.743 0-2.954.436-3.632 1.308-.678.872-1.018 1.938-1.018 3.197 0 1.26.34 2.325 1.018 3.197.678.872 1.889 1.308 3.632 1.308h8.138v72.075c0 .193.024.339.073.436.048.096.072.242.072.436H46.56c-1.744 0-3.003.435-3.778 1.307-.775.872-1.163 1.938-1.163 3.197 0 1.356.388 2.446 1.163 3.27.775.823 2.034 1.235 3.778 1.235h34.875z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
src/assets/svgs/login-bg.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="5760" height="3040"><image width="5760" height="3040" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAFoAAAAvgAQMAAAC1QKagAAAABGdBTUEAALGPC/xhBQAAACBjSFJN AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABlBMVEUsNEr///91v/yPAAAA AWJLR0QB/wIt3gAAAAd0SU1FB+YBBQYyN1c3BnEAAAhjSURBVHja7cExAQAAAMKg9U9tDB+gAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAACAtwFzzwABY3VrRQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMi0wMS0wNVQwNjo1 MDo1MyswMDowMCfNlVoAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjItMDEtMDVUMDY6NTA6NTQrMDA6 MDCTNxNoAAAAAElFTkSuQmCC"/></svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
1
src/assets/svgs/login-box-bg.svg
Normal file
|
After Width: | Height: | Size: 33 KiB |
1
src/assets/svgs/message.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 20.967v59.59c0 11.59 8.537 20.966 19.075 20.966h28.613l1 26.477L76.8 101.523h32.125c10.538 0 19.075-9.377 19.075-20.966v-59.59C128 9.377 119.463 0 108.925 0h-89.85C8.538 0 0 9.377 0 20.967zm82.325 33.1c0-5.524 4.013-9.935 9.037-9.935 5.026 0 9.038 4.41 9.038 9.934 0 5.524-4.025 9.934-9.038 9.934-5.024 0-9.037-4.41-9.037-9.934zm-27.613 0c0-5.524 4.013-9.935 9.038-9.935s9.037 4.41 9.037 9.934c0 5.524-4.025 9.934-9.037 9.934-5.025 0-9.038-4.41-9.038-9.934zm-27.1 0c0-5.524 4.013-9.935 9.038-9.935s9.038 4.41 9.038 9.934c0 5.524-4.026 9.934-9.05 9.934-5.013 0-9.025-4.41-9.025-9.934z"/></svg>
|
||||
|
After Width: | Height: | Size: 669 B |
1
src/assets/svgs/money.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M54.122 127.892v-28.68H7.513V87.274h46.609v-12.4H7.513v-12.86h38.003L.099 0h22.6l32.556 45.07c3.617 5.144 6.44 9.611 8.487 13.385 1.788-3.05 4.89-7.779 9.301-14.186L103.93 0h24.01L82.385 62.013h38.34v12.862h-46.41v12.4h46.41v11.937h-46.41v28.68H54.123z"/></svg>
|
||||
|
After Width: | Height: | Size: 335 B |
1
src/assets/svgs/peoples.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M95.648 118.762c0 5.035-3.563 9.121-7.979 9.121H7.98c-4.416 0-7.979-4.086-7.979-9.121C0 100.519 15.408 83.47 31.152 76.75c-9.099-6.43-15.216-17.863-15.216-30.987v-9.128c0-20.16 14.293-36.518 31.893-36.518s31.894 16.358 31.894 36.518v9.122c0 13.137-6.123 24.556-15.216 30.993 15.738 6.726 31.141 23.769 31.141 42.012z"/><path d="M106.032 118.252h15.867c3.376 0 6.101-3.125 6.101-6.972 0-13.957-11.787-26.984-23.819-32.123 6.955-4.919 11.638-13.66 11.638-23.704v-6.985c0-15.416-10.928-27.926-24.39-27.926-1.674 0-3.306.193-4.89.561 1.936 4.713 3.018 9.974 3.018 15.526v9.121c0 13.137-3.056 23.111-11.066 30.993 14.842 4.41 27.312 23.42 27.541 41.509z"/></svg>
|
||||
|
After Width: | Height: | Size: 731 B |
1
src/assets/svgs/shopping.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M42.913 101.36c1.642 0 3.198.332 4.667.996a12.28 12.28 0 013.89 2.772c1.123 1.184 1.987 2.582 2.592 4.193.605 1.612.908 3.318.908 5.118 0 1.8-.303 3.507-.908 5.118-.605 1.611-1.469 3.01-2.593 4.194a13.3 13.3 0 01-3.889 2.843 10.582 10.582 0 01-4.667 1.066c-1.729 0-3.306-.355-4.732-1.066a13.604 13.604 0 01-3.825-2.843c-1.123-1.185-1.988-2.583-2.593-4.194a14.437 14.437 0 01-.907-5.118c0-1.8.302-3.506.907-5.118.605-1.61 1.47-3.009 2.593-4.193a12.515 12.515 0 013.825-2.772c1.426-.664 3.003-.996 4.732-.996zm53.932.285c1.643 0 3.22.331 4.733.995a11.386 11.386 0 013.889 2.772c1.08 1.185 1.945 2.583 2.593 4.194.648 1.61.972 3.317.972 5.118 0 1.8-.324 3.506-.972 5.117-.648 1.611-1.513 3.01-2.593 4.194a12.253 12.253 0 01-3.89 2.843 11 11 0 01-4.732 1.066 10.58 10.58 0 01-4.667-1.066 12.478 12.478 0 01-3.824-2.843c-1.08-1.185-1.945-2.583-2.593-4.194a13.581 13.581 0 01-.973-5.117c0-1.801.325-3.507.973-5.118.648-1.611 1.512-3.01 2.593-4.194a11.559 11.559 0 013.824-2.772 11.212 11.212 0 014.667-.995zm21.781-80.747c2.42 0 4.3.355 5.64 1.066 1.34.71 2.29 1.587 2.852 2.63a6.427 6.427 0 01.778 3.34c-.044 1.185-.195 2.204-.454 3.057-.26.853-.8 2.606-1.62 5.26a589.268 589.268 0 01-2.788 8.743 1236.373 1236.373 0 00-3.047 9.453c-.994 3.128-1.75 5.592-2.269 7.393-1.123 3.79-2.55 6.42-4.278 7.89-1.728 1.469-3.846 2.203-6.352 2.203H39.023l1.945 12.795h65.342c4.148 0 6.223 1.943 6.223 5.828 0 1.896-.41 3.53-1.232 4.905-.821 1.374-2.442 2.061-4.862 2.061H38.505c-1.729 0-3.176-.426-4.343-1.28-1.167-.852-2.14-1.966-2.917-3.34a21.277 21.277 0 01-1.88-4.478 44.128 44.128 0 01-1.102-4.55c-.087-.568-.324-1.942-.713-4.122-.39-2.18-.865-4.904-1.426-8.174l-1.88-10.947c-.692-4.027-1.383-8.079-2.075-12.154-1.642-9.572-3.5-20.234-5.574-31.986H6.87c-1.296 0-2.377-.356-3.24-1.067a9.024 9.024 0 01-2.14-2.558 10.416 10.416 0 01-1.167-3.2C.108 8.53 0 7.488 0 6.54c0-1.896.583-3.46 1.75-4.69C2.917.615 4.494 0 6.482 0h13.095c1.728 0 3.111.284 4.148.853 1.037.569 1.858 1.28 2.463 2.132a8.548 8.548 0 011.297 2.701c.26.948.475 1.754.648 2.417.173.758.346 1.825.519 3.199.173 1.374.345 2.772.518 4.193.26 1.706.519 3.507.778 5.403h88.678z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
3
src/components/Backtop/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Backtop from './src/Backtop.vue'
|
||||
|
||||
export { Backtop }
|
||||
15
src/components/Backtop/src/Backtop.vue
Normal file
@@ -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>
|
||||
3
src/components/ConfigGlobal/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import ConfigGlobal from './src/ConfigGlobal.vue'
|
||||
|
||||
export { ConfigGlobal }
|
||||
61
src/components/ConfigGlobal/src/ConfigGlobal.vue
Normal file
@@ -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>
|
||||
3
src/components/ContentDetailWrap/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import ContentDetailWrap from './src/ContentDetailWrap.vue'
|
||||
|
||||
export { ContentDetailWrap }
|
||||
56
src/components/ContentDetailWrap/src/ContentDetailWrap.vue
Normal file
@@ -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>
|
||||
3
src/components/ContentWrap/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import ContentWrap from './src/ContentWrap.vue'
|
||||
|
||||
export { ContentWrap }
|
||||
32
src/components/ContentWrap/src/ContentWrap.vue
Normal file
@@ -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>
|
||||
3
src/components/CountTo/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import CountTo from './src/CountTo.vue'
|
||||
|
||||
export { CountTo }
|
||||
180
src/components/CountTo/src/CountTo.vue
Normal file
@@ -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>
|
||||
2
src/components/Crontab/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import Crontab from './src/Crontab.vue'
|
||||
export { Crontab }
|
||||
1009
src/components/Crontab/src/Crontab.vue
Normal file
4
src/components/Cropper/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import CropperImage from './src/Cropper.vue'
|
||||
import CropperAvatar from './src/CropperAvatar.vue'
|
||||
|
||||
export { CropperImage, CropperAvatar }
|
||||
257
src/components/Cropper/src/CopperModal.vue
Normal file
@@ -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>
|
||||
181
src/components/Cropper/src/Cropper.vue
Normal file
@@ -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>
|
||||
141
src/components/Cropper/src/CropperAvatar.vue
Normal file
@@ -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>
|
||||
8
src/components/Cropper/src/types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type Cropper from 'cropperjs'
|
||||
|
||||
export interface CropendResult {
|
||||
imgBase64: string
|
||||
imgInfo: Cropper.Data
|
||||
}
|
||||
|
||||
export type { Cropper }
|
||||
3
src/components/Descriptions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Descriptions from './src/Descriptions.vue'
|
||||
|
||||
export { Descriptions }
|
||||
172
src/components/Descriptions/src/Descriptions.vue
Normal file
@@ -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>
|
||||
3
src/components/Dialog/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Dialog from './src/Dialog.vue'
|
||||
|
||||
export { Dialog }
|
||||
126
src/components/Dialog/src/Dialog.vue
Normal file
@@ -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>
|
||||
3
src/components/DictTag/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import DictTag from './src/DictTag.vue'
|
||||
|
||||
export { DictTag }
|
||||
60
src/components/DictTag/src/DictTag.vue
Normal file
@@ -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>
|
||||
32
src/components/DocAlert/index.vue
Normal file
@@ -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>
|
||||
3
src/components/Echart/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Echart from './src/Echart.vue'
|
||||
|
||||
export { Echart }
|
||||
114
src/components/Echart/src/Echart.vue
Normal file
@@ -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>
|
||||
8
src/components/Editor/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import Editor from './src/Editor.vue'
|
||||
import { IDomEditor } from '@wangeditor/editor'
|
||||
|
||||
export interface EditorExpose {
|
||||
getEditorRef: () => Promise<IDomEditor>
|
||||
}
|
||||
|
||||
export { Editor }
|
||||
280
src/components/Editor/src/Editor.vue
Normal file
@@ -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>
|
||||
68
src/components/Editor/src/README.md
Normal file
@@ -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" // 全屏
|
||||
]
|
||||
3
src/components/Error/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Error from './src/Error.vue'
|
||||
|
||||
export { Error }
|
||||
56
src/components/Error/src/Error.vue
Normal file
@@ -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>
|
||||
15
src/components/Form/index.ts
Normal file
@@ -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 }
|
||||
314
src/components/Form/src/Form.vue
Normal file
@@ -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>
|
||||
55
src/components/Form/src/componentMap.ts
Normal file
@@ -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 }
|
||||
26
src/components/Form/src/components/useRenderCheckbox.tsx
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
26
src/components/Form/src/components/useRenderRadio.tsx
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
57
src/components/Form/src/components/useRenderSelect.tsx
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
148
src/components/Form/src/helper.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import type { Slots } from 'vue'
|
||||
import { getSlot } from '@/utils/tsxHelper'
|
||||
import { PlaceholderModel } from './types'
|
||||
import { FormSchema } from '@/types/form'
|
||||
import { ColProps } from '@/types/components'
|
||||
|
||||
/**
|
||||
*
|
||||
* @param schema 对应组件数据
|
||||
* @returns 返回提示信息对象
|
||||
* @description 用于自动设置placeholder
|
||||
*/
|
||||
export const setTextPlaceholder = (schema: FormSchema): PlaceholderModel => {
|
||||
const { t } = useI18n()
|
||||
const textMap = ['Input', 'Autocomplete', 'InputNumber', 'InputPassword']
|
||||
const selectMap = ['Select', 'SelectV2', 'TimePicker', 'DatePicker', 'TimeSelect', 'TimeSelect']
|
||||
if (textMap.includes(schema?.component as string)) {
|
||||
return {
|
||||
placeholder: t('common.inputText') + schema.label
|
||||
}
|
||||
}
|
||||
if (selectMap.includes(schema?.component as string)) {
|
||||
// 一些范围选择器
|
||||
const twoTextMap = ['datetimerange', 'daterange', 'monthrange', 'datetimerange', 'daterange']
|
||||
if (
|
||||
twoTextMap.includes(
|
||||
(schema?.componentProps?.type || schema?.componentProps?.isRange) as string
|
||||
)
|
||||
) {
|
||||
return {
|
||||
startPlaceholder: t('common.startTimeText'),
|
||||
endPlaceholder: t('common.endTimeText'),
|
||||
rangeSeparator: '-'
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
placeholder: t('common.selectText') + schema.label
|
||||
}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param col 内置栅格
|
||||
* @returns 返回栅格属性
|
||||
* @description 合并传入进来的栅格属性
|
||||
*/
|
||||
export const setGridProp = (col: ColProps = {}): ColProps => {
|
||||
const colProps: ColProps = {
|
||||
// 如果有span,代表用户优先级更高,所以不需要默认栅格
|
||||
...(col.span
|
||||
? {}
|
||||
: {
|
||||
xs: 24,
|
||||
sm: 12,
|
||||
md: 12,
|
||||
lg: 12,
|
||||
xl: 12
|
||||
}),
|
||||
...col
|
||||
}
|
||||
return colProps
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param item 传入的组件属性
|
||||
* @returns 默认添加 clearable 属性
|
||||
*/
|
||||
export const setComponentProps = (item: FormSchema): Recordable => {
|
||||
const notNeedClearable = ['ColorPicker']
|
||||
const componentProps: Recordable = notNeedClearable.includes(item.component as string)
|
||||
? { ...item.componentProps }
|
||||
: {
|
||||
clearable: true,
|
||||
...item.componentProps
|
||||
}
|
||||
// 需要删除额外的属性
|
||||
delete componentProps?.slots
|
||||
return componentProps
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param slots 插槽
|
||||
* @param slotsProps 插槽属性
|
||||
* @param field 字段名
|
||||
*/
|
||||
export const setItemComponentSlots = (
|
||||
slots: Slots,
|
||||
slotsProps: Recordable = {},
|
||||
field: string
|
||||
): Recordable => {
|
||||
const slotObj: Recordable = {}
|
||||
for (const key in slotsProps) {
|
||||
if (slotsProps[key]) {
|
||||
// 由于组件有可能重复,需要有一个唯一的前缀
|
||||
slotObj[key] = (data: Recordable) => {
|
||||
return getSlot(slots, `${field}-${key}`, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
return slotObj
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param schema Form表单结构化数组
|
||||
* @param formModel FormModel
|
||||
* @returns FormModel
|
||||
* @description 生成对应的formModel
|
||||
*/
|
||||
export const initModel = (schema: FormSchema[], formModel: Recordable) => {
|
||||
const model: Recordable = { ...formModel }
|
||||
schema.map((v) => {
|
||||
// 如果是hidden,就删除对应的值
|
||||
if (v.hidden) {
|
||||
delete model[v.field]
|
||||
} else if (v.component && v.component !== 'Divider') {
|
||||
const hasField = Reflect.has(model, v.field)
|
||||
// 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值
|
||||
model[v.field] = hasField ? model[v.field] : v.value !== void 0 ? v.value : ''
|
||||
}
|
||||
})
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* @param slots 插槽
|
||||
* @param field 字段名
|
||||
* @returns 返回FormIiem插槽
|
||||
*/
|
||||
export const setFormItemSlots = (slots: Slots, field: string): Recordable => {
|
||||
const slotObj: Recordable = {}
|
||||
if (slots[`${field}-error`]) {
|
||||
slotObj['error'] = (data: Recordable) => {
|
||||
return getSlot(slots, `${field}-error`, data)
|
||||
}
|
||||
}
|
||||
if (slots[`${field}-label`]) {
|
||||
slotObj['label'] = (data: Recordable) => {
|
||||
return getSlot(slots, `${field}-label`, data)
|
||||
}
|
||||
}
|
||||
return slotObj
|
||||
}
|
||||