sc
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=/tiku-api
|
||||||
|
|
||||||
|
# 接口地址
|
||||||
|
VITE_API_URL=/admin-api
|
||||||
|
|
||||||
|
# 打包路径
|
||||||
|
VITE_BASE_PATH=/tiku/
|
||||||
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=/tiku-api
|
||||||
|
|
||||||
|
# 接口地址
|
||||||
|
VITE_API_URL=/admin-api
|
||||||
|
|
||||||
|
# 打包路径
|
||||||
|
VITE_BASE_PATH=/tiku/
|
||||||
|
|
||||||
|
# 是否删除debugger
|
||||||
|
VITE_DROP_DEBUGGER=false
|
||||||
|
|
||||||
|
# 是否删除console.log
|
||||||
|
VITE_DROP_CONSOLE=false
|
||||||
|
|
||||||
|
# 是否sourcemap
|
||||||
|
VITE_SOURCEMAP=true
|
||||||
|
|
||||||
|
# 输出路径
|
||||||
|
VITE_OUT_DIR=dist-dev
|
||||||
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=/tiku-api
|
||||||
|
|
||||||
|
# 接口地址
|
||||||
|
VITE_API_URL=/admin-api
|
||||||
|
|
||||||
|
# 打包路径
|
||||||
|
VITE_BASE_PATH=/tiku/
|
||||||
|
|
||||||
|
# 项目本地运行端口号, 与.vscode/launch.json配合
|
||||||
|
VITE_PORT=80
|
||||||
|
|
||||||
|
# 是否删除debugger
|
||||||
|
VITE_DROP_DEBUGGER=false
|
||||||
|
|
||||||
|
# 是否删除console.log
|
||||||
|
VITE_DROP_CONSOLE=false
|
||||||
|
|
||||||
|
# 是否sourcemap
|
||||||
|
VITE_SOURCEMAP=true
|
||||||
|
|
||||||
|
# 验证码的开关
|
||||||
|
VITE_APP_CAPTCHA_ENABLE=false
|
||||||
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=/tiku-api
|
||||||
|
|
||||||
|
# 接口地址
|
||||||
|
VITE_API_URL=/admin-api
|
||||||
|
|
||||||
|
# 是否删除debugger
|
||||||
|
VITE_DROP_DEBUGGER=true
|
||||||
|
|
||||||
|
# 是否删除console.log
|
||||||
|
VITE_DROP_CONSOLE=true
|
||||||
|
|
||||||
|
# 是否sourcemap
|
||||||
|
VITE_SOURCEMAP=false
|
||||||
|
|
||||||
|
# 打包路径
|
||||||
|
VITE_BASE_PATH=/tiku/
|
||||||
|
|
||||||
|
# 输出路径
|
||||||
|
VITE_OUT_DIR=dist-pro
|
||||||
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=/tiku-api
|
||||||
|
|
||||||
|
# 接口地址
|
||||||
|
VITE_API_URL=/admin-api
|
||||||
|
|
||||||
|
# 是否删除debugger
|
||||||
|
VITE_DROP_DEBUGGER=true
|
||||||
|
|
||||||
|
# 是否删除console.log
|
||||||
|
VITE_DROP_CONSOLE=true
|
||||||
|
|
||||||
|
# 是否sourcemap
|
||||||
|
VITE_SOURCEMAP=false
|
||||||
|
|
||||||
|
# 打包路径
|
||||||
|
VITE_BASE_PATH='/tiku/'
|
||||||
|
|
||||||
|
# 输出路径
|
||||||
|
VITE_OUT_DIR=dist-stage
|
||||||
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=/tiku-api
|
||||||
|
|
||||||
|
# 接口地址
|
||||||
|
VITE_API_URL=/admin-api
|
||||||
|
|
||||||
|
# 是否删除debugger
|
||||||
|
VITE_DROP_DEBUGGER=true
|
||||||
|
|
||||||
|
# 是否删除console.log
|
||||||
|
VITE_DROP_CONSOLE=true
|
||||||
|
|
||||||
|
# 是否sourcemap
|
||||||
|
VITE_SOURCEMAP=false
|
||||||
|
|
||||||
|
# 打包路径
|
||||||
|
VITE_BASE_PATH=/tiku/
|
||||||
|
|
||||||
|
# 输出路径
|
||||||
|
VITE_OUT_DIR=dist-dev
|
||||||
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=/tiku-api
|
||||||
|
|
||||||
|
# 接口地址
|
||||||
|
VITE_API_URL=/admin-api
|
||||||
|
|
||||||
|
# 是否删除debugger
|
||||||
|
VITE_DROP_DEBUGGER=false
|
||||||
|
|
||||||
|
# 是否删除console.log
|
||||||
|
VITE_DROP_CONSOLE=false
|
||||||
|
|
||||||
|
# 是否sourcemap
|
||||||
|
VITE_SOURCEMAP=true
|
||||||
|
|
||||||
|
# 打包路径
|
||||||
|
VITE_BASE_PATH=/tiku/
|
||||||
|
|
||||||
|
# 输出路径
|
||||||
|
VITE_OUT_DIR=dist-test
|
||||||
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'
|
||||||
|
}
|
||||||
|
})
|
||||||
22
.gitignore
vendored
@@ -1,11 +1,11 @@
|
|||||||
# ---> Vue
|
node_modules
|
||||||
# gitignore template for Vue.js projects
|
.DS_Store
|
||||||
#
|
dist
|
||||||
# Recommended template: Node.gitignore
|
dist-ssr
|
||||||
|
*.local
|
||||||
# TODO: where does this rule come from?
|
/dist*
|
||||||
docs/_book
|
*-lock.*
|
||||||
|
pnpm-debug
|
||||||
# TODO: where does this rule come from?
|
auto-*.d.ts
|
||||||
test/
|
.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-tiku-manage",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"description": "莳松题库管理系统",
|
||||||
|
"author": "ss",
|
||||||
|
"private": false,
|
||||||
|
"scripts": {
|
||||||
|
"i": "pnpm install",
|
||||||
|
"dev": "vite --mode base",
|
||||||
|
"dev-front": "vite --mode front",
|
||||||
|
"front": "vite --mode front",
|
||||||
|
"ts:check": "vue-tsc --noEmit",
|
||||||
|
"build:pro": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode pro",
|
||||||
|
"build:dev": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode dev",
|
||||||
|
"build:stage": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode stage",
|
||||||
|
"build:test": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode test",
|
||||||
|
"build:static": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode static",
|
||||||
|
"build:front": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode front",
|
||||||
|
"serve:pro": "vite preview --mode pro",
|
||||||
|
"serve:dev": "vite preview --mode dev",
|
||||||
|
"serve:test": "vite preview --mode test",
|
||||||
|
"preview": "pnpm build && vite preview",
|
||||||
|
"npm:check": "npx npm-check-updates",
|
||||||
|
"clean": "npx rimraf node_modules",
|
||||||
|
"clean:cache": "npx rimraf node_modules/.cache",
|
||||||
|
"lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src",
|
||||||
|
"lint:format": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,scss,vue,html,md}\"",
|
||||||
|
"lint:style": "stylelint --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
|
||||||
|
"lint:lint-staged": "lint-staged -c ",
|
||||||
|
"lint:pretty": "pretty-quick --staged"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@amap/amap-jsapi-loader": "^1.0.1",
|
||||||
|
"@element-plus/icons-vue": "^2.1.0",
|
||||||
|
"@form-create/designer": "^3.1.0",
|
||||||
|
"@form-create/element-ui": "^3.1.17",
|
||||||
|
"@iconify/iconify": "^3.1.0",
|
||||||
|
"@videojs-player/vue": "^1.0.0",
|
||||||
|
"@vueuse/core": "^10.1.2",
|
||||||
|
"@wangeditor/editor": "^5.1.23",
|
||||||
|
"@wangeditor/editor-for-vue": "^5.1.10",
|
||||||
|
"@zxcvbn-ts/core": "^3.0.1",
|
||||||
|
"animate.css": "^4.1.1",
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"benz-amr-recorder": "^1.1.5",
|
||||||
|
"bpmn-js-token-simulation": "^0.10.0",
|
||||||
|
"camunda-bpmn-moddle": "^7.0.1",
|
||||||
|
"cropperjs": "^1.5.13",
|
||||||
|
"crypto-js": "^4.1.1",
|
||||||
|
"dayjs": "^1.11.7",
|
||||||
|
"diagram-js": "^11.6.0",
|
||||||
|
"echarts": "^5.4.2",
|
||||||
|
"echarts-wordcloud": "^2.1.0",
|
||||||
|
"element-plus": "2.9.4",
|
||||||
|
"fast-xml-parser": "^4.2.2",
|
||||||
|
"highlight.js": "^11.8.0",
|
||||||
|
"intro.js": "^7.0.1",
|
||||||
|
"jsencrypt": "^3.3.2",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"min-dash": "^4.1.1",
|
||||||
|
"mitt": "^3.0.0",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"pinia": "^2.1.3",
|
||||||
|
"qrcode": "^1.5.3",
|
||||||
|
"qs": "^6.11.2",
|
||||||
|
"steady-xml": "^0.1.0",
|
||||||
|
"url": "^0.11.0",
|
||||||
|
"video.js": "^8.3.0",
|
||||||
|
"vue": "3.3.4",
|
||||||
|
"vue-amap": "^0.5.10",
|
||||||
|
"vue-dompurify-html": "^5.0.1",
|
||||||
|
"vue-i18n": "9.2.2",
|
||||||
|
"vue-router": "^4.2.1",
|
||||||
|
"vue-types": "^5.0.3",
|
||||||
|
"vue3-tree-org": "^4.2.2",
|
||||||
|
"vuedraggable": "^4.1.0",
|
||||||
|
"web-storage-cache": "^1.1.1",
|
||||||
|
"xe-utils": "^3.5.7",
|
||||||
|
"xml-js": "^1.6.11"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^17.6.3",
|
||||||
|
"@commitlint/config-conventional": "^17.6.3",
|
||||||
|
"@iconify/json": "^2.2.67",
|
||||||
|
"@intlify/unplugin-vue-i18n": "^0.10.0",
|
||||||
|
"@purge-icons/generated": "^0.9.0",
|
||||||
|
"@types/intro.js": "^5.1.1",
|
||||||
|
"@types/lodash-es": "^4.17.7",
|
||||||
|
"@types/node": "^18.16.0",
|
||||||
|
"@types/nprogress": "^0.2.0",
|
||||||
|
"@types/qrcode": "^1.5.0",
|
||||||
|
"@types/qs": "^6.9.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.59.6",
|
||||||
|
"@typescript-eslint/parser": "^5.59.6",
|
||||||
|
"@vitejs/plugin-legacy": "^4.0.3",
|
||||||
|
"@vitejs/plugin-vue": "^4.2.3",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"bpmn-js": "^8.9.0",
|
||||||
|
"bpmn-js-properties-panel": "^0.46.0",
|
||||||
|
"consola": "^3.1.0",
|
||||||
|
"eslint": "^8.40.0",
|
||||||
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
"eslint-define-config": "^1.20.0",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"eslint-plugin-vue": "^9.13.0",
|
||||||
|
"lint-staged": "^13.2.2",
|
||||||
|
"postcss": "^8.4.23",
|
||||||
|
"postcss-html": "^1.5.0",
|
||||||
|
"postcss-scss": "^4.0.6",
|
||||||
|
"prettier": "^2.8.8",
|
||||||
|
"rimraf": "^5.0.1",
|
||||||
|
"rollup": "^3.22.0",
|
||||||
|
"sass": "^1.62.1",
|
||||||
|
"stylelint": "^15.6.2",
|
||||||
|
"stylelint-config-html": "^1.1.0",
|
||||||
|
"stylelint-config-recommended": "^12.0.0",
|
||||||
|
"stylelint-config-standard": "^33.0.0",
|
||||||
|
"stylelint-order": "^6.0.3",
|
||||||
|
"terser": "^5.17.4",
|
||||||
|
"typescript": "5.0.4",
|
||||||
|
"unplugin-auto-import": "^0.16.0",
|
||||||
|
"unplugin-element-plus": "^0.7.1",
|
||||||
|
"unplugin-vue-components": "^0.24.1",
|
||||||
|
"vite": "4.3.8",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
|
"vite-plugin-ejs": "^1.6.4",
|
||||||
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
|
"vite-plugin-progress": "^0.0.7",
|
||||||
|
"vite-plugin-purge-icons": "^0.9.2",
|
||||||
|
"vite-plugin-svg-icons": "^2.0.1",
|
||||||
|
"vite-plugin-top-level-await": "^1.3.0",
|
||||||
|
"vite-plugin-vue-setup-extend-plus": "^0.1.0",
|
||||||
|
"vite-plugin-windicss": "^1.9.0",
|
||||||
|
"vue-tsc": "^1.6.5",
|
||||||
|
"windicss": "^3.5.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0"
|
||||||
|
},
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
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>
|
||||||
36
src/api/cutomer/customer.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
// 创建
|
||||||
|
export const createCustomer = (data) => {
|
||||||
|
return request.post({ url: '/admin-api/tiku/customer/create', data, isSubmitForm: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
export const getCustomerSimpleList = (params) => {
|
||||||
|
return request.get({ url: '/admin-api/tiku/customer/list-all-simple', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改
|
||||||
|
export const updateCustomer = (data) => {
|
||||||
|
return request.put({ url: '/admin-api/tiku/customer/update', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
export const getCustomerPage = (params) => {
|
||||||
|
return request.get({ url: '/admin-api/tiku/customer/page', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 详情
|
||||||
|
export const getCustomerDetail = (id) => {
|
||||||
|
return request.get({ url: '/admin-api/tiku/customer/get', params: { id } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
export const deleteCustomer = (id) => {
|
||||||
|
return request.delete({ url: '/admin-api/tiku/customer/delete', params: { id } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取钉钉id
|
||||||
|
export const getDingUserId = (params) => {
|
||||||
|
return request.get({ url: '/admin-api/tiku/customer/getDingTalkUserIdByMobile', params })
|
||||||
|
}
|
||||||
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/login', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新访问令牌
|
||||||
|
export const refreshToken = () => {
|
||||||
|
return request.post({
|
||||||
|
url: '/admin-api/system/auth/refresh-token?refreshToken=' + getRefreshToken()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用租户名,获得租户编号
|
||||||
|
export const getTenantIdByName = (name: string) => {
|
||||||
|
return request.get({ url: '/admin-api/system/tenant/get-id-by-name?name=' + name })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登出
|
||||||
|
export const loginOut = () => {
|
||||||
|
return request.post({ url: '/admin-api/system/auth/logout' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户权限信息
|
||||||
|
export const getInfo = (params) => {
|
||||||
|
return request.get({ url: '/admin-api/system/auth/get-permission-info', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取登录验证码
|
||||||
|
export const sendSmsCode = (data: SmsCodeVO) => {
|
||||||
|
return request.post({ url: '/admin-api/system/auth/send-sms-code', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 短信验证码登录
|
||||||
|
export const smsLogin = (data: SmsLoginVO) => {
|
||||||
|
return request.post({ url: '/admin-api/system/auth/sms-login', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 社交授权的跳转
|
||||||
|
export const socialAuthRedirect = (type: number, redirectUri: string) => {
|
||||||
|
return request.get({
|
||||||
|
url: '/admin-api/system/auth/social-auth-redirect?type=' + type + '&redirectUri=' + redirectUri
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 获取验证图片以及 token
|
||||||
|
export const getCode = (data) => {
|
||||||
|
return request.postOriginal({ url: '/admin-api/system/captcha/get', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滑动或者点选验证
|
||||||
|
export const reqCheck = (data) => {
|
||||||
|
return request.postOriginal({ url: '/admin-api/system/captcha/check', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取应用信息
|
||||||
|
export const getAppInfo = (instanceId: number) => {
|
||||||
|
return request.get({
|
||||||
|
url: '/admin-api/system/serviceInstance/getInstanceInfo?instanceId=' + instanceId
|
||||||
|
})
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||