qsh 2 months ago
parent e71c9928a2
commit f6e98e71e2
  1. 12
      .editorconfig
  2. 20
      .env
  3. 24
      .env.base
  4. 31
      .env.dev
  5. 34
      .env.front
  6. 31
      .env.pro
  7. 31
      .env.stage
  8. 31
      .env.static
  9. 31
      .env.test
  10. 8
      .eslintignore
  11. 259
      .eslintrc-auto-import.json
  12. 69
      .eslintrc.js
  13. 22
      .gitignore
  14. 11
      .prettierignore
  15. 6
      .stylelintignore
  16. 21
      LICENSE
  17. 4
      README.md
  18. 109
      build/vite/index.ts
  19. 111
      build/vite/optimize.ts
  20. 143
      index.html
  21. 142
      package.json
  22. 5
      postcss.config.js
  23. 22
      prettier.config.js
  24. BIN
      public/favicon.ico
  25. BIN
      public/logo.gif
  26. 54
      src/App.vue
  27. 36
      src/api/cutomer/customer.js
  28. 48
      src/api/infra/config/index.ts
  29. 45
      src/api/infra/file/index.ts
  30. 73
      src/api/login/index.ts
  31. 41
      src/api/login/oauth2/index.ts
  32. 28
      src/api/login/types.ts
  33. 5
      src/api/system/app/index.js
  34. 43
      src/api/system/dept/index.ts
  35. 54
      src/api/system/dict/dict.data.ts
  36. 44
      src/api/system/dict/dict.type.ts
  37. 54
      src/api/system/menu/index.ts
  38. 53
      src/api/system/notify/message/index.ts
  39. 49
      src/api/system/notify/template/index.ts
  40. 42
      src/api/system/permission/index.ts
  41. 53
      src/api/system/role/index.ts
  42. 16
      src/api/system/set/index.js
  43. 76
      src/api/system/user/index.ts
  44. 77
      src/api/system/user/profile.ts
  45. 31
      src/api/system/user/socialUser.ts
  46. BIN
      src/assets/fonts/DISPLAY FREE TFB.ttf
  47. BIN
      src/assets/fonts/字魂74号-飞墨手书_爱给网_aigei_com.ttf
  48. BIN
      src/assets/imgs/avatar.gif
  49. BIN
      src/assets/imgs/avatar.jpg
  50. BIN
      src/assets/imgs/login2.gif
  51. BIN
      src/assets/imgs/profile.jpg
  52. BIN
      src/assets/imgs/shisong.jpg
  53. BIN
      src/assets/imgs/wechat.png
  54. 1
      src/assets/svgs/403.svg
  55. 1
      src/assets/svgs/404.svg
  56. 1
      src/assets/svgs/500.svg
  57. 1
      src/assets/svgs/icon.svg
  58. 1
      src/assets/svgs/login-bg.svg
  59. 1
      src/assets/svgs/login-box-bg.svg
  60. 1
      src/assets/svgs/message.svg
  61. 1
      src/assets/svgs/money.svg
  62. 1
      src/assets/svgs/peoples.svg
  63. 1
      src/assets/svgs/shopping.svg
  64. 3
      src/components/Backtop/index.ts
  65. 15
      src/components/Backtop/src/Backtop.vue
  66. 3
      src/components/ConfigGlobal/index.ts
  67. 61
      src/components/ConfigGlobal/src/ConfigGlobal.vue
  68. 3
      src/components/ContentDetailWrap/index.ts
  69. 56
      src/components/ContentDetailWrap/src/ContentDetailWrap.vue
  70. 3
      src/components/ContentWrap/index.ts
  71. 32
      src/components/ContentWrap/src/ContentWrap.vue
  72. 3
      src/components/CountTo/index.ts
  73. 180
      src/components/CountTo/src/CountTo.vue
  74. 2
      src/components/Crontab/index.ts
  75. 1009
      src/components/Crontab/src/Crontab.vue
  76. 4
      src/components/Cropper/index.ts
  77. 257
      src/components/Cropper/src/CopperModal.vue
  78. 181
      src/components/Cropper/src/Cropper.vue
  79. 141
      src/components/Cropper/src/CropperAvatar.vue
  80. 8
      src/components/Cropper/src/types.ts
  81. 3
      src/components/Descriptions/index.ts
  82. 172
      src/components/Descriptions/src/Descriptions.vue
  83. 3
      src/components/Dialog/index.ts
  84. 126
      src/components/Dialog/src/Dialog.vue
  85. 3
      src/components/DictTag/index.ts
  86. 60
      src/components/DictTag/src/DictTag.vue
  87. 32
      src/components/DocAlert/index.vue
  88. 3
      src/components/Echart/index.ts
  89. 114
      src/components/Echart/src/Echart.vue
  90. 8
      src/components/Editor/index.ts
  91. 280
      src/components/Editor/src/Editor.vue
  92. 68
      src/components/Editor/src/README.md
  93. 3
      src/components/Error/index.ts
  94. 56
      src/components/Error/src/Error.vue
  95. 15
      src/components/Form/index.ts
  96. 314
      src/components/Form/src/Form.vue
  97. 55
      src/components/Form/src/componentMap.ts
  98. 26
      src/components/Form/src/components/useRenderCheckbox.tsx
  99. 26
      src/components/Form/src/components/useRenderRadio.tsx
  100. 57
      src/components/Form/src/components/useRenderSelect.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

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

@ -0,0 +1,20 @@
# 标题
VITE_APP_TITLE=刷题后台
# 开发环境
VITE_NODE_ENV=development
# 项目本地运行端口号
VITE_PORT=8000
# open 运行 npm run dev 时自动打开浏览器
VITE_OPEN=true
# 租户开关
VITE_APP_TENANT_ENABLE=false
# 验证码的开关
VITE_APP_CAPTCHA_ENABLE=true
# 百度统计
VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc

@ -0,0 +1,24 @@
# 本地开发环境
VITE_NODE_ENV=development
VITE_DEV=true
# 请求路径
# VITE_BASE_URL='http://localhost:48080'
VITE_BASE_URL='http://47.98.161.246:48080'
# VITE_BASE_URL='http://114.55.169.15:48080'
# VITE_BASE_URL='http://114.215.207.150:48080'
# 上传路径
VITE_UPLOAD_URL='http://47.98.161.246:48080/admin-api/system/file/upload'
# VITE_UPLOAD_URL='http://114.55.169.15:48080/admin-api/system/file/upload'
# 接口前缀
VITE_API_BASEPATH=/tiku-api
# 接口地址
VITE_API_URL=/admin-api
# 打包路径
VITE_BASE_PATH=/tiku/

@ -0,0 +1,31 @@
# 开发环境
VITE_NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='http://localhost:48080'
# 上传路径
VITE_UPLOAD_URL='http://47.98.161.246:48080/admin-api/system/file/upload'
# 接口前缀
VITE_API_BASEPATH=/tiku-api
# 接口地址
VITE_API_URL=/admin-api
# 打包路径
VITE_BASE_PATH=/tiku/
# 是否删除debugger
VITE_DROP_DEBUGGER=false
# 是否删除console.log
VITE_DROP_CONSOLE=false
# 是否sourcemap
VITE_SOURCEMAP=true
# 输出路径
VITE_OUT_DIR=dist-dev

@ -0,0 +1,34 @@
# 本地开发环境
VITE_NODE_ENV=development
VITE_DEV=true
# 请求路径
VITE_BASE_URL='http://47.98.161.246:48080'
# 上传路径
VITE_UPLOAD_URL='http://47.98.161.246:48080/admin-api/system/file/upload'
# 接口前缀
VITE_API_BASEPATH=/tiku-api
# 接口地址
VITE_API_URL=/admin-api
# 打包路径
VITE_BASE_PATH=/tiku/
# 项目本地运行端口号, 与.vscode/launch.json配合
VITE_PORT=80
# 是否删除debugger
VITE_DROP_DEBUGGER=false
# 是否删除console.log
VITE_DROP_CONSOLE=false
# 是否sourcemap
VITE_SOURCEMAP=true
# 验证码的开关
VITE_APP_CAPTCHA_ENABLE=false

@ -0,0 +1,31 @@
# 生产环境
VITE_NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='/oa-api'
# 上传路径
VITE_UPLOAD_URL='/oa-api/admin-api/system/file/upload'
# 接口前缀
VITE_API_BASEPATH=/tiku-api
# 接口地址
VITE_API_URL=/admin-api
# 是否删除debugger
VITE_DROP_DEBUGGER=true
# 是否删除console.log
VITE_DROP_CONSOLE=true
# 是否sourcemap
VITE_SOURCEMAP=false
# 打包路径
VITE_BASE_PATH=/tiku/
# 输出路径
VITE_OUT_DIR=dist-pro

@ -0,0 +1,31 @@
# 生产环境
VITE_NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
# 上传路径
VITE_UPLOAD_URL='http://47.98.161.246:48080/admin-api/system/file/upload'
# 接口前缀
VITE_API_BASEPATH=/tiku-api
# 接口地址
VITE_API_URL=/admin-api
# 是否删除debugger
VITE_DROP_DEBUGGER=true
# 是否删除console.log
VITE_DROP_CONSOLE=true
# 是否sourcemap
VITE_SOURCEMAP=false
# 打包路径
VITE_BASE_PATH='/tiku/'
# 输出路径
VITE_OUT_DIR=dist-stage

@ -0,0 +1,31 @@
# 开发环境
VITE_NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='http://localhost:48080'
# 上传路径
VITE_UPLOAD_URL='http://47.98.161.246:48080/admin-api/system/file/upload'
# 接口前缀
VITE_API_BASEPATH=/tiku-api
# 接口地址
VITE_API_URL=/admin-api
# 是否删除debugger
VITE_DROP_DEBUGGER=true
# 是否删除console.log
VITE_DROP_CONSOLE=true
# 是否sourcemap
VITE_SOURCEMAP=false
# 打包路径
VITE_BASE_PATH=/tiku/
# 输出路径
VITE_OUT_DIR=dist-dev

@ -0,0 +1,31 @@
# 测试环境
VITE_NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='http://localhost:48080'
# 上传路径
VITE_UPLOAD_URL='http://47.98.161.246:48080/admin-api/system/file/upload'
# 接口前缀
VITE_API_BASEPATH=/tiku-api
# 接口地址
VITE_API_URL=/admin-api
# 是否删除debugger
VITE_DROP_DEBUGGER=false
# 是否删除console.log
VITE_DROP_CONSOLE=false
# 是否sourcemap
VITE_SOURCEMAP=true
# 打包路径
VITE_BASE_PATH=/tiku/
# 输出路径
VITE_OUT_DIR=dist-test

@ -0,0 +1,8 @@
/build/
/config/
/dist/
/*.js
/test/unit/coverage/
/node_modules/*
/dist*
/src/main.ts

@ -0,0 +1,259 @@
{
"globals": {
"EffectScope": true,
"ElMessage": true,
"ElMessageBox": true,
"ElTag": true,
"asyncComputed": true,
"autoResetRef": true,
"computed": true,
"computedAsync": true,
"computedEager": true,
"computedInject": true,
"computedWithControl": true,
"controlledComputed": true,
"controlledRef": true,
"createApp": true,
"createEventHook": true,
"createGlobalState": true,
"createInjectionState": true,
"createReactiveFn": true,
"createSharedComposable": true,
"createUnrefFn": true,
"customRef": true,
"debouncedRef": true,
"debouncedWatch": true,
"defineAsyncComponent": true,
"defineComponent": true,
"eagerComputed": true,
"effectScope": true,
"extendRef": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"ignorableWatch": true,
"inject": true,
"isDefined": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"makeDestructurable": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onClickOutside": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onKeyStroke": true,
"onLongPress": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onStartTyping": true,
"onUnmounted": true,
"onUpdated": true,
"pausableWatch": true,
"provide": true,
"reactify": true,
"reactifyObject": true,
"reactive": true,
"reactiveComputed": true,
"reactiveOmit": true,
"reactivePick": true,
"readonly": true,
"ref": true,
"refAutoReset": true,
"refDebounced": true,
"refDefault": true,
"refThrottled": true,
"refWithControl": true,
"resolveComponent": true,
"resolveRef": true,
"resolveUnref": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"syncRef": true,
"syncRefs": true,
"templateRef": true,
"throttledRef": true,
"throttledWatch": true,
"toRaw": true,
"toReactive": true,
"toRef": true,
"toRefs": true,
"triggerRef": true,
"tryOnBeforeMount": true,
"tryOnBeforeUnmount": true,
"tryOnMounted": true,
"tryOnScopeDispose": true,
"tryOnUnmounted": true,
"unref": true,
"unrefElement": true,
"until": true,
"useActiveElement": true,
"useArrayEvery": true,
"useArrayFilter": true,
"useArrayFind": true,
"useArrayFindIndex": true,
"useArrayJoin": true,
"useArrayMap": true,
"useArrayReduce": true,
"useArraySome": true,
"useAsyncQueue": true,
"useAsyncState": true,
"useAttrs": true,
"useBase64": true,
"useBattery": true,
"useBluetooth": true,
"useBreakpoints": true,
"useBroadcastChannel": true,
"useBrowserLocation": true,
"useCached": true,
"useClipboard": true,
"useColorMode": true,
"useConfirmDialog": true,
"useCounter": true,
"useCssModule": true,
"useCssVar": true,
"useCssVars": true,
"useCurrentElement": true,
"useCycleList": true,
"useDark": true,
"useDateFormat": true,
"useDebounce": true,
"useDebounceFn": true,
"useDebouncedRefHistory": true,
"useDeviceMotion": true,
"useDeviceOrientation": true,
"useDevicePixelRatio": true,
"useDevicesList": true,
"useDisplayMedia": true,
"useDocumentVisibility": true,
"useDraggable": true,
"useDropZone": true,
"useElementBounding": true,
"useElementByPoint": true,
"useElementHover": true,
"useElementSize": true,
"useElementVisibility": true,
"useEventBus": true,
"useEventListener": true,
"useEventSource": true,
"useEyeDropper": true,
"useFavicon": true,
"useFetch": true,
"useFileDialog": true,
"useFileSystemAccess": true,
"useFocus": true,
"useFocusWithin": true,
"useFps": true,
"useFullscreen": true,
"useGamepad": true,
"useGeolocation": true,
"useIdle": true,
"useImage": true,
"useInfiniteScroll": true,
"useIntersectionObserver": true,
"useInterval": true,
"useIntervalFn": true,
"useKeyModifier": true,
"useLastChanged": true,
"useLocalStorage": true,
"useMagicKeys": true,
"useManualRefHistory": true,
"useMediaControls": true,
"useMediaQuery": true,
"useMemoize": true,
"useMemory": true,
"useMounted": true,
"useMouse": true,
"useMouseInElement": true,
"useMousePressed": true,
"useMutationObserver": true,
"useNavigatorLanguage": true,
"useNetwork": true,
"useNow": true,
"useObjectUrl": true,
"useOffsetPagination": true,
"useOnline": true,
"usePageLeave": true,
"useParallax": true,
"usePermission": true,
"usePointer": true,
"usePointerSwipe": true,
"usePreferredColorScheme": true,
"usePreferredDark": true,
"usePreferredLanguages": true,
"useRafFn": true,
"useRefHistory": true,
"useResizeObserver": true,
"useRoute": true,
"useRouter": true,
"useScreenOrientation": true,
"useScreenSafeArea": true,
"useScriptTag": true,
"useScroll": true,
"useScrollLock": true,
"useSessionStorage": true,
"useShare": true,
"useSlots": true,
"useSpeechRecognition": true,
"useSpeechSynthesis": true,
"useStepper": true,
"useStorage": true,
"useStorageAsync": true,
"useStyleTag": true,
"useSupported": true,
"useSwipe": true,
"useTemplateRefsList": true,
"useTextDirection": true,
"useTextSelection": true,
"useTextareaAutosize": true,
"useThrottle": true,
"useThrottleFn": true,
"useThrottledRefHistory": true,
"useTimeAgo": true,
"useTimeout": true,
"useTimeoutFn": true,
"useTimeoutPoll": true,
"useTimestamp": true,
"useTitle": true,
"useToggle": true,
"useTransition": true,
"useUrlSearchParams": true,
"useUserMedia": true,
"useVModel": true,
"useVModels": true,
"useVibrate": true,
"useVirtualList": true,
"useWakeLock": true,
"useWebNotification": true,
"useWebSocket": true,
"useWebWorker": true,
"useWebWorkerFn": true,
"useWindowFocus": true,
"useWindowScroll": true,
"useWindowSize": true,
"watch": true,
"watchArray": true,
"watchAtMost": true,
"watchDebounced": true,
"watchEffect": true,
"watchIgnorable": true,
"watchOnce": true,
"watchPausable": true,
"watchPostEffect": true,
"watchSyncEffect": true,
"watchThrottled": true,
"watchTriggerable": true,
"watchWithFilter": true,
"whenever": true
}
}

@ -0,0 +1,69 @@
// @ts-check
const { defineConfig } = require('eslint-define-config')
module.exports = defineConfig({
root: true,
env: {
browser: true,
node: true,
es6: true
},
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 2020,
sourceType: 'module',
jsxPragma: 'React',
ecmaFeatures: {
jsx: true
}
},
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended',
'./.eslintrc-auto-import.json'
],
rules: {
'vue/script-setup-uses-vars': 'error',
'vue/no-reserved-component-names': 'off',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'vue/custom-event-name-casing': 'off',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'no-unused-vars': 'error',
'space-before-function-paren': 'off',
'vue/attributes-order': 'off',
'vue/one-component-per-file': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/max-attributes-per-line': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/attribute-hyphenation': 'off',
'vue/require-default-prop': 'off',
'vue/require-explicit-emits': 'off',
'vue/html-self-closing': [
'error',
{
html: {
void: 'always',
normal: 'never',
component: 'always'
},
svg: 'always',
math: 'always'
}
],
'vue/multi-word-component-names': 'off'
}
})

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

@ -0,0 +1,11 @@
/node_modules/**
/dist/
/dist*
/public/*
/docs/*
/vite.config.ts
/src/types/env.d.ts
/src/types/auto-components.d.ts
/src/types/auto-imports.d.ts
/docs/**/*
CHANGELOG

@ -0,0 +1,6 @@
/dist/*
/public/*
public/*
/dist*
/src/types/env.d.ts
/docs/**/*

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-present Archer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -1,3 +1 @@
# ss-tiku-manage-web ### 题库维护。刷题软件数据后台
题库维护。刷题软件数据后台

@ -0,0 +1,109 @@
import { resolve } from 'path'
import Vue from '@vitejs/plugin-vue'
import VueJsx from '@vitejs/plugin-vue-jsx'
import WindiCSS from 'vite-plugin-windicss'
import progress from 'vite-plugin-progress'
import EslintPlugin from 'vite-plugin-eslint'
import PurgeIcons from 'vite-plugin-purge-icons'
import { ViteEjsPlugin } from 'vite-plugin-ejs'
// @ts-ignore
import ElementPlus from 'unplugin-element-plus/vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import viteCompression from 'vite-plugin-compression'
import topLevelAwait from 'vite-plugin-top-level-await'
import vueSetupExtend from 'vite-plugin-vue-setup-extend-plus'
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
export function createVitePlugins() {
const root = process.cwd()
// 路径查找
function pathResolve(dir: string) {
return resolve(root, '.', dir)
}
return [
Vue(),
VueJsx(),
WindiCSS(),
progress(),
PurgeIcons(),
vueSetupExtend(),
ElementPlus({}),
AutoImport({
include: [
/\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
/\.vue$/,
/\.vue\?vue/, // .vue
/\.md$/ // .md
],
imports: [
'vue',
'vue-router',
// 可额外添加需要 autoImport 的组件
{
'@/hooks/web/useI18n': ['useI18n'],
'@/hooks/web/useMessage': ['useMessage'],
'@/hooks/web/useTable': ['useTable'],
'@/hooks/web/useCrudSchemas': ['useCrudSchemas'],
'@/utils/formRules': ['required'],
'@/utils/dict': ['DICT_TYPE']
}
],
dts: 'src/types/auto-imports.d.ts',
resolvers: [ElementPlusResolver()],
eslintrc: {
enabled: false, // Default `false`
filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
globalsPropValue: true // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
}
}),
Components({
// 要搜索组件的目录的相对路径
dirs: ['src/components'],
// 组件的有效文件扩展名
extensions: ['vue', 'md'],
// 搜索子目录
deep: true,
include: [/\.vue$/, /\.vue\?vue/],
// 生成自定义 `auto-components.d.ts` 全局声明
dts: 'src/types/auto-components.d.ts',
// 自定义组件的解析器
resolvers: [ElementPlusResolver()],
exclude: [/[\\/]node_modules[\\/]/]
}),
EslintPlugin({
cache: false,
include: ['src/**/*.vue', 'src/**/*.ts', 'src/**/*.tsx'] // 检查的文件
}),
VueI18nPlugin({
runtimeOnly: true,
compositionOnly: true,
include: [resolve(__dirname, 'src/locales/**')]
}),
createSvgIconsPlugin({
iconDirs: [pathResolve('src/assets/svgs')],
symbolId: 'icon-[dir]-[name]',
svgoOptions: true
}),
viteCompression({
verbose: true, // 是否在控制台输出压缩结果
disable: false, // 是否禁用
threshold: 10240, // 体积大于 threshold 才会被压缩,单位 b
algorithm: 'gzip', // 压缩算法,可选 [ 'gzip' , 'brotliCompress' ,'deflate' , 'deflateRaw']
ext: '.gz', // 生成的压缩包后缀
deleteOriginFile: false //压缩后是否删除源文件
}),
ViteEjsPlugin(),
topLevelAwait({
// https://juejin.cn/post/7152191742513512485
// The export name of top-level await promise for each chunk module
promiseExportName: '__tla',
// The function to generate import names of top-level await promise in each chunk module
promiseImportName: (i) => `__tla_${i}`
})
]
}

@ -0,0 +1,111 @@
const include = [
'qs',
'url',
'vue',
'sass',
'mitt',
'axios',
'pinia',
'dayjs',
'qrcode',
'windicss',
'vue-router',
'vue-types',
'vue-i18n',
'xe-utils',
'crypto-js',
'cropperjs',
'lodash-es',
'nprogress',
'web-storage-cache',
'@iconify/iconify',
'@vueuse/core',
'@zxcvbn-ts/core',
'echarts/core',
'echarts/charts',
'echarts/components',
'echarts/renderers',
'echarts-wordcloud',
'@wangeditor/editor',
'@wangeditor/editor-for-vue',
'element-plus',
'element-plus/es',
'element-plus/es/locale/lang/zh-cn',
'element-plus/es/locale/lang/en',
'element-plus/es/components/backtop/style/css',
'element-plus/es/components/form/style/css',
'element-plus/es/components/radio-group/style/css',
'element-plus/es/components/radio/style/css',
'element-plus/es/components/checkbox/style/css',
'element-plus/es/components/checkbox-group/style/css',
'element-plus/es/components/switch/style/css',
'element-plus/es/components/time-picker/style/css',
'element-plus/es/components/date-picker/style/css',
'element-plus/es/components/descriptions/style/css',
'element-plus/es/components/descriptions-item/style/css',
'element-plus/es/components/link/style/css',
'element-plus/es/components/tooltip/style/css',
'element-plus/es/components/drawer/style/css',
'element-plus/es/components/dialog/style/css',
'element-plus/es/components/checkbox-button/style/css',
'element-plus/es/components/option-group/style/css',
'element-plus/es/components/radio-button/style/css',
'element-plus/es/components/cascader/style/css',
'element-plus/es/components/color-picker/style/css',
'element-plus/es/components/input-number/style/css',
'element-plus/es/components/rate/style/css',
'element-plus/es/components/select-v2/style/css',
'element-plus/es/components/tree-select/style/css',
'element-plus/es/components/slider/style/css',
'element-plus/es/components/time-select/style/css',
'element-plus/es/components/autocomplete/style/css',
'element-plus/es/components/image-viewer/style/css',
'element-plus/es/components/upload/style/css',
'element-plus/es/components/col/style/css',
'element-plus/es/components/form-item/style/css',
'element-plus/es/components/alert/style/css',
'element-plus/es/components/breadcrumb/style/css',
'element-plus/es/components/select/style/css',
'element-plus/es/components/input/style/css',
'element-plus/es/components/breadcrumb-item/style/css',
'element-plus/es/components/tag/style/css',
'element-plus/es/components/pagination/style/css',
'element-plus/es/components/table/style/css',
'element-plus/es/components/table-v2/style/css',
'element-plus/es/components/table-column/style/css',
'element-plus/es/components/card/style/css',
'element-plus/es/components/row/style/css',
'element-plus/es/components/button/style/css',
'element-plus/es/components/menu/style/css',
'element-plus/es/components/sub-menu/style/css',
'element-plus/es/components/menu-item/style/css',
'element-plus/es/components/option/style/css',
'element-plus/es/components/dropdown/style/css',
'element-plus/es/components/dropdown-menu/style/css',
'element-plus/es/components/dropdown-item/style/css',
'element-plus/es/components/skeleton/style/css',
'element-plus/es/components/skeleton/style/css',
'element-plus/es/components/backtop/style/css',
'element-plus/es/components/menu/style/css',
'element-plus/es/components/sub-menu/style/css',
'element-plus/es/components/menu-item/style/css',
'element-plus/es/components/dropdown/style/css',
'element-plus/es/components/tree/style/css',
'element-plus/es/components/dropdown-menu/style/css',
'element-plus/es/components/dropdown-item/style/css',
'element-plus/es/components/badge/style/css',
'element-plus/es/components/breadcrumb/style/css',
'element-plus/es/components/breadcrumb-item/style/css',
'element-plus/es/components/image/style/css',
'element-plus/es/components/collapse-transition/style/css',
'element-plus/es/components/timeline/style/css',
'element-plus/es/components/timeline-item/style/css',
'element-plus/es/components/collapse/style/css',
'element-plus/es/components/collapse-item/style/css',
'element-plus/es/components/button-group/style/css',
'element-plus/es/components/text/style/css'
]
const exclude = ['@iconify/json']
export { include, exclude }

@ -0,0 +1,143 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>%VITE_APP_TITLE%</title>
</head>
<body>
<div id="app">
<style>
.app-loading {
display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
flex-direction: column;
background: #f0f2f5;
}
.app-loading .app-loading-wrap {
position: absolute;
top: 50%;
left: 50%;
display: flex;
-webkit-transform: translate3d(-50%, -50%, 0);
transform: translate3d(-50%, -50%, 0);
justify-content: center;
align-items: center;
flex-direction: column;
}
.app-loading .app-loading-title {
margin-bottom: 30px;
font-size: 20px;
font-weight: bold;
text-align: center;
}
.app-loading .app-loading-logo {
width: 100px;
margin: 0 auto 15px auto;
}
.app-loading .app-loading-item {
position: relative;
display: inline-block;
width: 60px;
height: 60px;
vertical-align: middle;
border-radius: 50%;
}
.app-loading .app-loading-outter {
position: absolute;
width: 100%;
height: 100%;
border: 4px solid #2d8cf0;
border-bottom: 0;
border-left-color: transparent;
border-radius: 50%;
animation: loader-outter 1s cubic-bezier(0.42, 0.61, 0.58, 0.41) infinite;
}
.app-loading .app-loading-inner {
position: absolute;
top: calc(50% - 20px);
left: calc(50% - 20px);
width: 40px;
height: 40px;
border: 4px solid #87bdff;
border-right: 0;
border-top-color: transparent;
border-radius: 50%;
animation: loader-inner 1s cubic-bezier(0.42, 0.61, 0.58, 0.41) infinite;
}
@-webkit-keyframes loader-outter {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loader-outter {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes loader-inner {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(-360deg);
transform: rotate(-360deg);
}
}
@keyframes loader-inner {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(-360deg);
transform: rotate(-360deg);
}
}
</style>
<div class="app-loading">
<div class="app-loading-wrap">
<div class="app-loading-title">
<img src="/logo.gif" class="app-loading-logo" alt="Logo" />
<div class="app-loading-title">%VITE_APP_TITLE%</div>
</div>
<div class="app-loading-item">
<div class="app-loading-outter"></div>
<div class="app-loading-inner"></div>
</div>
</div>
</div>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

@ -0,0 +1,142 @@
{
"name": "ss-tiku-manage",
"version": "1.0.1",
"description": "莳松题库管理系统",
"author": "ss",
"private": false,
"scripts": {
"i": "pnpm install",
"dev": "vite --mode base",
"dev-front": "vite --mode front",
"front": "vite --mode front",
"ts:check": "vue-tsc --noEmit",
"build:pro": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode pro",
"build:dev": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode dev",
"build:stage": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode stage",
"build:test": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode test",
"build:static": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode static",
"build:front": "node --max_old_space_size=8000 ./node_modules/vite/bin/vite.js build --mode front",
"serve:pro": "vite preview --mode pro",
"serve:dev": "vite preview --mode dev",
"serve:test": "vite preview --mode test",
"preview": "pnpm build && vite preview",
"npm:check": "npx npm-check-updates",
"clean": "npx rimraf node_modules",
"clean:cache": "npx rimraf node_modules/.cache",
"lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src",
"lint:format": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,scss,vue,html,md}\"",
"lint:style": "stylelint --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
"lint:lint-staged": "lint-staged -c ",
"lint:pretty": "pretty-quick --staged"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@element-plus/icons-vue": "^2.1.0",
"@form-create/designer": "^3.1.0",
"@form-create/element-ui": "^3.1.17",
"@iconify/iconify": "^3.1.0",
"@videojs-player/vue": "^1.0.0",
"@vueuse/core": "^10.1.2",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^3.0.1",
"animate.css": "^4.1.1",
"axios": "^1.4.0",
"benz-amr-recorder": "^1.1.5",
"bpmn-js-token-simulation": "^0.10.0",
"camunda-bpmn-moddle": "^7.0.1",
"cropperjs": "^1.5.13",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.7",
"diagram-js": "^11.6.0",
"echarts": "^5.4.2",
"echarts-wordcloud": "^2.1.0",
"element-plus": "2.9.4",
"fast-xml-parser": "^4.2.2",
"highlight.js": "^11.8.0",
"intro.js": "^7.0.1",
"jsencrypt": "^3.3.2",
"lodash-es": "^4.17.21",
"min-dash": "^4.1.1",
"mitt": "^3.0.0",
"nprogress": "^0.2.0",
"pinia": "^2.1.3",
"qrcode": "^1.5.3",
"qs": "^6.11.2",
"steady-xml": "^0.1.0",
"url": "^0.11.0",
"video.js": "^8.3.0",
"vue": "3.3.4",
"vue-amap": "^0.5.10",
"vue-dompurify-html": "^5.0.1",
"vue-i18n": "9.2.2",
"vue-router": "^4.2.1",
"vue-types": "^5.0.3",
"vue3-tree-org": "^4.2.2",
"vuedraggable": "^4.1.0",
"web-storage-cache": "^1.1.1",
"xe-utils": "^3.5.7",
"xml-js": "^1.6.11"
},
"devDependencies": {
"@commitlint/cli": "^17.6.3",
"@commitlint/config-conventional": "^17.6.3",
"@iconify/json": "^2.2.67",
"@intlify/unplugin-vue-i18n": "^0.10.0",
"@purge-icons/generated": "^0.9.0",
"@types/intro.js": "^5.1.1",
"@types/lodash-es": "^4.17.7",
"@types/node": "^18.16.0",
"@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.5.0",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^5.59.6",
"@typescript-eslint/parser": "^5.59.6",
"@vitejs/plugin-legacy": "^4.0.3",
"@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^3.0.1",
"autoprefixer": "^10.4.14",
"bpmn-js": "^8.9.0",
"bpmn-js-properties-panel": "^0.46.0",
"consola": "^3.1.0",
"eslint": "^8.40.0",
"eslint-config-prettier": "^8.8.0",
"eslint-define-config": "^1.20.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.13.0",
"lint-staged": "^13.2.2",
"postcss": "^8.4.23",
"postcss-html": "^1.5.0",
"postcss-scss": "^4.0.6",
"prettier": "^2.8.8",
"rimraf": "^5.0.1",
"rollup": "^3.22.0",
"sass": "^1.62.1",
"stylelint": "^15.6.2",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recommended": "^12.0.0",
"stylelint-config-standard": "^33.0.0",
"stylelint-order": "^6.0.3",
"terser": "^5.17.4",
"typescript": "5.0.4",
"unplugin-auto-import": "^0.16.0",
"unplugin-element-plus": "^0.7.1",
"unplugin-vue-components": "^0.24.1",
"vite": "4.3.8",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-ejs": "^1.6.4",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-progress": "^0.0.7",
"vite-plugin-purge-icons": "^0.9.2",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-top-level-await": "^1.3.0",
"vite-plugin-vue-setup-extend-plus": "^0.1.0",
"vite-plugin-windicss": "^1.9.0",
"vue-tsc": "^1.6.5",
"windicss": "^3.5.6"
},
"engines": {
"node": ">=16.0.0"
},
"license": "MIT"
}

@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}

@ -0,0 +1,22 @@
module.exports = {
printWidth: 100, // 每行代码长度(默认80)
tabWidth: 2, // 每个tab相当于多少个空格(默认2)ab进行缩进(默认false)
useTabs: false, // 是否使用tab
semi: false, // 声明结尾使用分号(默认true)
vueIndentScriptAndStyle: false,
singleQuote: true, // 使用单引号(默认false)
quoteProps: 'as-needed',
bracketSpacing: true, // 对象字面量的大括号间使用空格(默认true)
trailingComma: 'none', // 多行使用拖尾逗号(默认none)
jsxSingleQuote: false,
// 箭头函数参数括号 默认avoid 可选 avoid| always
// avoid 能省略括号的时候就省略 例如x => x
// always 总是有括号
arrowParens: 'always',
insertPragma: false,
requirePragma: false,
proseWrap: 'never',
htmlWhitespaceSensitivity: 'strict',
endOfLine: 'auto',
rangeStart: 0
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

@ -0,0 +1,54 @@
<script lang="ts" name="APP" setup>
import { isDark } from '@/utils/is'
import { useAppStore } from '@/store/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
import { CACHE_KEY } from '@/hooks/web/useCache'
import routerSearch from '@/components/RouterSearch/index.vue'
import cache from '@/plugins/cache'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('app')
const appStore = useAppStore()
const currentSize = computed(() => appStore.getCurrentSize)
const greyMode = computed(() => appStore.getGreyMode)
//
const setDefaultTheme = () => {
let isDarkTheme = cache.local.get(CACHE_KEY.IS_DARK)
if (isDarkTheme === null) {
isDarkTheme = isDark()
}
appStore.setIsDark(isDarkTheme)
}
setDefaultTheme()
</script>
<template>
<ConfigGlobal :size="currentSize">
<RouterView :class="greyMode ? `${prefixCls}-grey-mode` : ''" />
<routerSearch />
</ConfigGlobal>
</template>
<style lang="scss">
$prefix-cls: #{$namespace}-app;
.size {
width: 100%;
height: 100%;
}
html,
body {
padding: 0 !important;
margin: 0;
overflow: hidden;
@extend .size;
#app {
@extend .size;
}
}
.#{$prefix-cls}-grey-mode {
filter: grayscale(100%);
}
</style>

@ -0,0 +1,36 @@
import request from '@/config/axios'
// 创建
export const createCustomer = (data) => {
return request.post({ url: '/admin-api/tiku/customer/create', data, isSubmitForm: true })
}
// 分页
export const getCustomerSimpleList = (params) => {
return request.get({ url: '/admin-api/tiku/customer/list-all-simple', params })
}
// 修改
export const updateCustomer = (data) => {
return request.put({ url: '/admin-api/tiku/customer/update', data })
}
// 分页
export const getCustomerPage = (params) => {
return request.get({ url: '/admin-api/tiku/customer/page', params })
}
// 详情
export const getCustomerDetail = (id) => {
return request.get({ url: '/admin-api/tiku/customer/get', params: { id } })
}
// 删除
export const deleteCustomer = (id) => {
return request.delete({ url: '/admin-api/tiku/customer/delete', params: { id } })
}
// 获取钉钉id
export const getDingUserId = (params) => {
return request.get({ url: '/admin-api/tiku/customer/getDingTalkUserIdByMobile', params })
}

@ -0,0 +1,48 @@
import request from '@/config/axios'
export interface ConfigVO {
id: number | undefined
category: string
name: string
key: string
value: string
type: number
visible: boolean
remark: string
createTime: Date
}
// 查询参数列表
export const getConfigPage = (params: PageParam) => {
return request.get({ url: '/admin-api/infra/config/page', params })
}
// 查询参数详情
export const getConfig = (id: number) => {
return request.get({ url: '/admin-api/infra/config/get?id=' + id })
}
// 根据参数键名查询参数值
export const getConfigKey = (configKey: string) => {
return request.get({ url: '/admin-api/infra/config/get-value-by-key?key=' + configKey })
}
// 新增参数
export const createConfig = (data: ConfigVO) => {
return request.post({ url: '/admin-api/infra/config/create', data })
}
// 修改参数
export const updateConfig = (data: ConfigVO) => {
return request.put({ url: '/admin-api/infra/config/update', data })
}
// 删除参数
export const deleteConfig = (id: number) => {
return request.delete({ url: '/admin-api/infra/config/delete?id=' + id })
}
// 导出参数
export const exportConfig = (params) => {
return request.download({ url: '/admin-api/infra/config/export', params })
}

@ -0,0 +1,45 @@
import request from '@/config/axios'
export interface FilePageReqVO extends PageParam {
path?: string
type?: string
createTime?: Date[]
}
// 文件预签名地址 Response VO
export interface FilePresignedUrlRespVO {
// 文件配置编号
configId: number
// 文件上传 URL
uploadUrl: string
// 文件 URL
url: string
}
// 查询文件列表
export const getFilePage = (params: FilePageReqVO) => {
return request.get({ url: '/infra/file/page', params })
}
// 删除文件
export const deleteFile = (id: number) => {
return request.delete({ url: '/infra/file/delete?id=' + id })
}
// 获取文件预签名地址
export const getFilePresignedUrl = (path: string) => {
return request.get<FilePresignedUrlRespVO>({
url: '/infra/file/presigned-url',
params: { path }
})
}
// 创建文件
export const createFile = (data: any) => {
return request.post({ url: '/infra/file/create', data })
}
// 上传文件
export const updateFile = (data: any) => {
return request.upload({ url: '/admin-api/system/file/upload', data })
}

@ -0,0 +1,73 @@
import request from '@/config/axios'
import { getRefreshToken } from '@/utils/auth'
import type { UserLoginVO } from './types'
export interface SmsCodeVO {
mobile: string
scene: number
}
export interface SmsLoginVO {
mobile: string
code: string
}
// 登录
export const login = (data: UserLoginVO) => {
return request.post({ url: '/admin-api/system/auth/login', data })
}
// 刷新访问令牌
export const refreshToken = () => {
return request.post({
url: '/admin-api/system/auth/refresh-token?refreshToken=' + getRefreshToken()
})
}
// 使用租户名,获得租户编号
export const getTenantIdByName = (name: string) => {
return request.get({ url: '/admin-api/system/tenant/get-id-by-name?name=' + name })
}
// 登出
export const loginOut = () => {
return request.post({ url: '/admin-api/system/auth/logout' })
}
// 获取用户权限信息
export const getInfo = (params) => {
return request.get({ url: '/admin-api/system/auth/get-permission-info', params })
}
//获取登录验证码
export const sendSmsCode = (data: SmsCodeVO) => {
return request.post({ url: '/admin-api/system/auth/send-sms-code', data })
}
// 短信验证码登录
export const smsLogin = (data: SmsLoginVO) => {
return request.post({ url: '/admin-api/system/auth/sms-login', data })
}
// 社交授权的跳转
export const socialAuthRedirect = (type: number, redirectUri: string) => {
return request.get({
url: '/admin-api/system/auth/social-auth-redirect?type=' + type + '&redirectUri=' + redirectUri
})
}
// 获取验证图片以及 token
export const getCode = (data) => {
return request.postOriginal({ url: '/admin-api/system/captcha/get', data })
}
// 滑动或者点选验证
export const reqCheck = (data) => {
return request.postOriginal({ url: '/admin-api/system/captcha/check', data })
}
// 获取应用信息
export const getAppInfo = (instanceId: number) => {
return request.get({
url: '/admin-api/system/serviceInstance/getInstanceInfo?instanceId=' + instanceId
})
}

@ -0,0 +1,41 @@
import request from '@/config/axios'
// 获得授权信息
export const getAuthorize = (clientId: string) => {
return request.get({ url: '/admin-api/system/oauth2/authorize?clientId=' + clientId })
}
// 发起授权
export const authorize = (
responseType: string,
clientId: string,
redirectUri: string,
state: string,
autoApprove: boolean,
checkedScopes: string[],
uncheckedScopes: string[]
) => {
// 构建 scopes
const scopes = {}
for (const scope of checkedScopes) {
scopes[scope] = true
}
for (const scope of uncheckedScopes) {
scopes[scope] = false
}
// 发起请求
return request.post({
url: '/admin-api/system/oauth2/authorize',
headers: {
'Content-type': 'application/x-www-form-urlencoded'
},
params: {
response_type: responseType,
client_id: clientId,
redirect_uri: redirectUri,
state: state,
auto_approve: autoApprove,
scope: JSON.stringify(scopes)
}
})
}

@ -0,0 +1,28 @@
export type UserLoginVO = {
username: string
password: string
captchaVerification: string
}
export type TokenType = {
id: number // 编号
accessToken: string // 访问令牌
refreshToken: string // 刷新令牌
userId: number // 用户编号
userType: number //用户类型
clientId: string //客户端编号
expiresTime: number //过期时间
}
export type UserVO = {
id: number
username: string
nickname: string
deptId: number
email: string
mobile: string
sex: number
avatar: string
loginIp: string
loginDate: string
}

@ -0,0 +1,5 @@
import request from '@/config/axios'
export const getSimpleAppList = async () => {
return await request.get({ url: '/admin-api/system/serviceInstance/simple-list' })
}

@ -0,0 +1,43 @@
import request from '@/config/axios'
export interface DeptVO {
id?: number
name: string
parentId: number
status: number
sort: number
leaderUserId: number
phone: string
email: string
createTime: Date
}
// 查询部门(精简)列表
export const getSimpleDeptList = async (params: any): Promise<any[]> => {
return await request.get({ url: '/admin-api/system/dept/list-all-simple', params })
}
// 查询部门列表
export const getDeptPage = async (params) => {
return await request.get({ url: '/admin-api/system/dept/list', params })
}
// 查询部门详情
export const getDept = async (id: number) => {
return await request.get({ url: '/admin-api/system/dept/get?id=' + id })
}
// 新增部门
export const createDept = async (data: DeptVO) => {
return await request.post({ url: '/admin-api/system/dept/create', data: data })
}
// 修改部门
export const updateDept = async (params: DeptVO) => {
return await request.put({ url: '/admin-api/system/dept/update', data: params })
}
// 删除部门
export const deleteDept = async (id: number) => {
return await request.delete({ url: '/admin-api/system/dept/delete?id=' + id })
}

@ -0,0 +1,54 @@
import request from '@/config/axios'
export type DictDataVO = {
id: number | undefined
sort: number | undefined
label: string
value: string
dictType: string
status: number
colorType: string
cssClass: string
remark: string
createTime: Date
}
// 查询字典数据(精简)列表
export const listSimpleDictData = () => {
return request.get({ url: '/admin-api/oa/dict-data/simple-list' })
}
// 查询字典数据列表
export const getDictDataPage = (params: PageParam) => {
return request.get({ url: '/admin-api/oa/dict-data/page', params })
}
// 查询字典数据详情
export const getDictData = (id: number) => {
return request.get({ url: '/admin-api/oa/dict-data/get?id=' + id })
}
// 新增字典数据
export const createDictData = (data: DictDataVO) => {
return request.post({ url: '/admin-api/oa/dict-data/create', data })
}
// 修改字典数据
export const updateDictData = (data: DictDataVO) => {
return request.put({ url: '/admin-api/oa/dict-data/update', data })
}
// 删除字典数据
export const deleteDictData = (id: number) => {
return request.delete({ url: '/admin-api/oa/dict-data/delete?id=' + id })
}
// 导出字典类型数据
export const exportDictData = (params) => {
return request.get({ url: '/admin-api/oa/dict-data/export', params })
}
// 获取通用字典数据
export const getGeneralSysDictData = (dictType: string) => {
return request.get({ url: '/admin-api/system/dict-data/get-by-type', params: { dictType } })
}

@ -0,0 +1,44 @@
import request from '@/config/axios'
export type DictTypeVO = {
id: number | undefined
name: string
type: string
status: number
remark: string
createTime: Date
}
// 查询字典(精简)列表
export const getSimpleDictTypeList = () => {
return request.get({ url: '/admin-api/oa/dict-type/list-all-simple' })
}
// 查询字典列表
export const getDictTypePage = (params: PageParam) => {
return request.get({ url: '/admin-api/oa/dict-type/page', params })
}
// 查询字典详情
export const getDictType = (id: number) => {
return request.get({ url: '/admin-api/oa/dict-type/get?id=' + id })
}
// 新增字典
export const createDictType = (data: DictTypeVO) => {
return request.post({ url: '/admin-api/oa/dict-type/create', data })
}
// 修改字典
export const updateDictType = (data: DictTypeVO) => {
return request.put({ url: '/admin-api/oa/dict-type/update', data })
}
// 删除字典
export const deleteDictType = (id: number) => {
return request.delete({ url: '/admin-api/oa/dict-type/delete?id=' + id })
}
// 导出字典类型
export const exportDictType = (params) => {
return request.get({ url: '/admin-api/oa/dict-type/export', params })
}

@ -0,0 +1,54 @@
import request from '@/config/axios'
export interface MenuVO {
id: number
name: string
permission: string
type: number
sort: number
parentId: number
path: string
icon: string
component: string
componentName?: string
status: number
visible: boolean
keepAlive: boolean
alwaysShow?: boolean
createTime: Date
}
// 获取服务列表
export const getServiceAppList = () => {
return request.get({ url: '/admin-api/system/service/list' })
}
// 查询菜单(精简)列表
export const getSimpleMenusList = () => {
return request.get({ url: '/admin-api/system/menu/list-all-simple' })
}
// 查询菜单列表
export const getMenuList = (params) => {
return request.get({ url: '/admin-api/system/menu/list', params })
}
// 获取菜单详情
export const getMenu = (id: number) => {
return request.get({ url: '/admin-api/system/menu/get?id=' + id })
}
// 新增菜单
export const createMenu = (data: MenuVO) => {
return request.post({ url: '/admin-api/system/menu/create', data })
}
// 修改菜单
export const updateMenu = (data: MenuVO) => {
return request.put({ url: '/admin-api/system/menu/update', data })
}
// 删除菜单
export const deleteMenu = (id: number) => {
return request.delete({ url: '/admin-api/system/menu/delete?id=' + id })
}

@ -0,0 +1,53 @@
import request from '@/config/axios'
export interface NotifyMessageVO {
id: number
userId: number
userType: number
templateId: number
templateCode: string
templateNickname: string
templateContent: string
templateType: number
templateParams: string
readStatus: boolean
readTime: Date
}
// 查询站内信消息列表
export const getNotifyMessagePage = async (params: any) => {
return await request.get({ url: '/admin-api/system/notify-message/page', params })
}
// 获得我的站内信分页
export const getMyNotifyMessagePage = async (params: any) => {
return await request.get({ url: '/admin-api/system/notify-message/my-page', params })
}
// 批量标记已读
export const updateNotifyMessageRead = async (data: any) => {
return await request.put({
url: '/admin-api/system/notify-message/update-read?',
data
})
}
// 标记所有站内信为已读
export const updateAllNotifyMessageRead = async (data: any) => {
return await request.put({ url: '/admin-api/system/notify-message/update-all-read', data })
}
// 获取当前用户的最新站内信列表
export const getUnreadNotifyMessageList = async (params: any) => {
return await request.get({ url: '/admin-api/system/notify-message/get-unread-list', params })
}
// 获得当前用户的未读站内信数量
export const getUnreadNotifyMessageCount = async (params: any) => {
return await request.get({ url: '/admin-api/system/notify-message/get-unread-count', params })
}
// 获取详情
export const getNotifyMessageDetail = async (id: number) => {
return await request.get({ url: '/admin-api/system/notify-message/get', params: { id } })
}

@ -0,0 +1,49 @@
import request from '@/config/axios'
export interface NotifyTemplateVO {
id?: number
name: string
nickname: string
code: string
content: string
type: number
params: string
status: number
remark: string
}
export interface NotifySendReqVO {
userId: number | null
templateCode: string
templateParams: Map<String, Object>
}
// 查询站内信模板列表
export const getNotifyTemplatePage = async (params: PageParam) => {
return await request.get({ url: '/system/notify-template/page', params })
}
// 查询站内信模板详情
export const getNotifyTemplate = async (id: number) => {
return await request.get({ url: '/system/notify-template/get?id=' + id })
}
// 新增站内信模板
export const createNotifyTemplate = async (data: NotifyTemplateVO) => {
return await request.post({ url: '/system/notify-template/create', data })
}
// 修改站内信模板
export const updateNotifyTemplate = async (data: NotifyTemplateVO) => {
return await request.put({ url: '/system/notify-template/update', data })
}
// 删除站内信模板
export const deleteNotifyTemplate = async (id: number) => {
return await request.delete({ url: '/system/notify-template/delete?id=' + id })
}
// 发送站内信
export const sendNotify = (data: NotifySendReqVO) => {
return request.post({ url: '/system/notify-template/send-notify', data })
}

@ -0,0 +1,42 @@
import request from '@/config/axios'
export interface PermissionAssignUserRoleReqVO {
userId: number
roleIds: number[]
}
export interface PermissionAssignRoleMenuReqVO {
roleId: number
menuIds: number[]
}
export interface PermissionAssignRoleDataScopeReqVO {
roleId: number
dataScope: number
dataScopeDeptIds: number[]
}
// 查询角色拥有的菜单权限
export const getRoleMenuList = async (roleId: number) => {
return await request.get({ url: '/admin-api/system/permission/list-role-menus?roleId=' + roleId })
}
// 赋予角色菜单权限
export const assignRoleMenu = async (data: PermissionAssignRoleMenuReqVO) => {
return await request.post({ url: '/admin-api/system/permission/assign-role-menu', data })
}
// 赋予角色数据权限
export const assignRoleDataScope = async (data: PermissionAssignRoleDataScopeReqVO) => {
return await request.post({ url: '/admin-api/system/permission/assign-role-data-scope', data })
}
// 查询用户拥有的角色数组
export const getUserRoleList = async (userId: number) => {
return await request.get({ url: '/admin-api/system/permission/list-user-roles?userId=' + userId })
}
// 赋予用户角色
export const assignUserRole = async (data: PermissionAssignUserRoleReqVO) => {
return await request.post({ url: '/admin-api/system/permission/assign-user-role', data })
}

@ -0,0 +1,53 @@
import request from '@/config/axios'
export interface RoleVO {
id: number
name: string
code: string
sort: number
status: number
type: number
dataScope: number
dataScopeDeptIds: number[]
createTime: Date
}
export interface UpdateStatusReqVO {
id: number
status: number
}
// 查询角色列表
export const getRolePage = async (params: PageParam) => {
return await request.get({ url: '/admin-api/system/role/page', params })
}
// 查询角色(精简)列表
export const getSimpleRoleList = async () => {
return await request.get({ url: '/admin-api/system/role/list-all-simple' })
}
// 查询角色详情
export const getRole = async (id: number) => {
return await request.get({ url: '/admin-api/system/role/get?id=' + id })
}
// 新增角色
export const createRole = async (data: RoleVO) => {
return await request.post({ url: '/admin-api/system/role/create', data })
}
// 修改角色
export const updateRole = async (data: RoleVO) => {
return await request.put({ url: '/admin-api/system/role/update', data })
}
// 删除角色
export const deleteRole = async (id: number) => {
return await request.delete({ url: '/admin-api/system/role/delete?id=' + id })
}
// 角色用户
export const getRoleUsers = async (params) => {
return await request.get({ url: '/admin-api/system/role/getUserByRole', params })
}

@ -0,0 +1,16 @@
import request from '@/config/axios'
// 通过key,查询内容
export const getConfigByConfigKey = (params) => {
return request.get({ url: '/admin-api/crm/config/getConfigByConfigKey', params })
}
// 保存配置项
export const updateConfig = (data) => {
return request.put({ url: '/admin-api/crm/config/batchUpdateConfigValue', data })
}
// 根据模块获取配置列表
export const getConfigList = (params) => {
return request.get({ url: '/admin-api/crm/config/query', params })
}

@ -0,0 +1,76 @@
import request from '@/config/axios'
export interface UserVO {
id: number
username: string
nickname: string
deptId: number
postIds: string[]
email: string
mobile: string
sex: number
avatar: string
loginIp: string
status: number
remark: string
loginDate: Date
createTime: Date
}
// 查询用户管理列表
export const getUserPage = (params: PageParam) => {
return request.get({ url: '/admin-api/system/user/page', params })
}
// 查询用户详情
export const getUser = (id: number) => {
return request.get({ url: '/admin-api/system/user/get?id=' + id })
}
// 新增用户
export const createUser = (data: UserVO) => {
return request.post({ url: '/admin-api/system/user/create', data })
}
// 修改用户
export const updateUser = (data: UserVO) => {
return request.put({ url: '/admin-api/system/user/update', data })
}
// 删除用户
export const deleteUser = (id: number) => {
return request.delete({ url: '/admin-api/system/user/delete?id=' + id })
}
// 导出用户
export const exportUser = (params) => {
return request.download({ url: '/admin-api/system/user/export', params })
}
// 下载用户导入模板
export const importUserTemplate = () => {
return request.download({ url: '/admin-api/system/user/get-import-template' })
}
// 用户密码重置
export const resetUserPwd = (id: number, password: string) => {
const data = {
id,
password
}
return request.put({ url: '/admin-api/system/user/update-password', data: data })
}
// 用户状态修改
export const updateUserStatus = (id: number, status: number) => {
const data = {
id,
status
}
return request.put({ url: '/admin-api/system/user/update-status', data: data })
}
// 获取用户精简信息列表
export const getSimpleUserList = (): Promise<UserVO[]> => {
return request.get({ url: '/admin-api/system/user/list-all-simple' })
}

@ -0,0 +1,77 @@
import request from '@/config/axios'
export interface ProfileDept {
id: number
name: string
}
export interface ProfileRole {
id: number
name: string
}
export interface ProfilePost {
id: number
name: string
}
export interface SocialUser {
id: number
type: number
openid: string
token: string
rawTokenInfo: string
nickname: string
avatar: string
rawUserInfo: string
code: string
state: string
}
export interface ProfileVO {
id: number
username: string
nickname: string
dept: ProfileDept
roles: ProfileRole[]
posts: ProfilePost[]
socialUsers: SocialUser[]
email: string
mobile: string
sex: number
avatar: string
status: number
remark: string
loginIp: string
loginDate: Date
createTime: Date
}
export interface UserProfileUpdateReqVO {
nickname: string
email: string
mobile: string
sex: number
}
// 查询用户个人信息
export const getUserProfile = () => {
return request.get({ url: '/admin-api/system/user/profile/get' })
}
// 修改用户个人信息
export const updateUserProfile = (data: UserProfileUpdateReqVO) => {
return request.put({ url: '/admin-api/system/user/profile/update', data })
}
// 用户密码重置
export const updateUserPassword = (oldPassword: string, newPassword: string) => {
return request.put({
url: '/admin-api/system/user/profile/update-password',
data: {
oldPassword: oldPassword,
newPassword: newPassword
}
})
}
// 用户头像上传
export const uploadAvatar = (data) => {
return request.upload({ url: '/admin-api/system/user/profile/update-avatar', data: data })
}

@ -0,0 +1,31 @@
import request from '@/config/axios'
// 社交绑定,使用 code 授权码
export const socialBind = (type, code, state) => {
return request.post({
url: '/admin-api/system/social-user/bind',
data: {
type,
code,
state
}
})
}
// 取消社交绑定
export const socialUnbind = (type, openid) => {
return request.delete({
url: '/admin-api/system/social-user/unbind',
data: {
type,
openid
}
})
}
// 社交授权的跳转
export const socialAuthRedirect = (type, redirectUri) => {
return request.get({
url: '/admin-api/system/auth/social-auth-redirect?type=' + type + '&redirectUri=' + redirectUri
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

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

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 33 KiB

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

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

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

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

@ -0,0 +1,3 @@
import Backtop from './src/Backtop.vue'
export { Backtop }

@ -0,0 +1,15 @@
<script lang="ts" name="BackTop" setup>
import { ElBacktop } from 'element-plus'
import { useDesign } from '@/hooks/web/useDesign'
const { getPrefixCls, variables } = useDesign()
const prefixCls = getPrefixCls('backtop')
</script>
<template>
<ElBacktop
:class="`${prefixCls}-backtop`"
:target="`.${variables.namespace}-layout-content-scrollbar .${variables.elNamespace}-scrollbar__wrap`"
/>
</template>

@ -0,0 +1,3 @@
import ConfigGlobal from './src/ConfigGlobal.vue'
export { ConfigGlobal }

@ -0,0 +1,61 @@
<script lang="ts" name="ConfigGlobal" setup>
import { propTypes } from '@/utils/propTypes'
import { useLocaleStore } from '@/store/modules/locale'
import { useAppStore } from '@/store/modules/app'
import { setCssVar } from '@/utils'
import { useDesign } from '@/hooks/web/useDesign'
import { ElementPlusSize } from '@/types/elementPlus'
import { useWindowSize } from '@vueuse/core'
const { variables } = useDesign()
const appStore = useAppStore()
const props = defineProps({
size: propTypes.oneOf<ElementPlusSize>(['default', 'small', 'large']).def('default')
})
provide('configGlobal', props)
//
onMounted(() => {
appStore.setCssVarTheme()
})
const { width } = useWindowSize()
//
watch(
() => width.value,
(width: number) => {
if (width < 768) {
!appStore.getMobile ? appStore.setMobile(true) : undefined
setCssVar('--left-menu-min-width', '0')
appStore.setCollapse(true)
appStore.getLayout !== 'classic' ? appStore.setLayout('classic') : undefined
} else {
appStore.getMobile ? appStore.setMobile(false) : undefined
setCssVar('--left-menu-min-width', '64px')
}
},
{
immediate: true
}
)
//
const localeStore = useLocaleStore()
const currentLocale = computed(() => localeStore.currentLocale)
</script>
<template>
<ElConfigProvider
:locale="currentLocale.elLocale"
:message="{ max: 1 }"
:namespace="variables.elNamespace"
:size="size"
>
<slot></slot>
</ElConfigProvider>
</template>

@ -0,0 +1,3 @@
import ContentDetailWrap from './src/ContentDetailWrap.vue'
export { ContentDetailWrap }

@ -0,0 +1,56 @@
<script lang="ts" name="ContentDetailWrap" setup>
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
const { t } = useI18n()
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('content-detail-wrap')
defineProps({
title: propTypes.string.def(''),
message: propTypes.string.def('')
})
const emit = defineEmits(['back'])
const offset = ref(85)
const contentDetailWrap = ref()
onMounted(() => {
offset.value = contentDetailWrap.value.getBoundingClientRect().top
})
</script>
<template>
<div ref="contentDetailWrap" :class="[`${prefixCls}-container`]">
<Sticky :offset="offset">
<div
:class="[
`${prefixCls}-header`,
'flex border-bottom-1 h-50px items-center text-center pr-10px'
]"
>
<div :class="[`${prefixCls}-header__back`, 'flex pl-10px pr-10px ']">
<ElButton @click="emit('back')">
<Icon class="mr-5px" icon="ep:arrow-left" />
{{ t('common.back') }}
</ElButton>
</div>
<div :class="[`${prefixCls}-header__title`, 'flex flex-1 justify-center']">
<slot name="title">
<label class="text-16px font-700">{{ title }}</label>
</slot>
</div>
<div :class="[`${prefixCls}-header__right`, 'flex pl-10px pr-10px']">
<slot name="right"></slot>
</div>
</div>
</Sticky>
<div style="padding: var(--app-content-padding)">
<ElCard :class="[`${prefixCls}-body`, 'mb-20px']" shadow="never">
<div>
<slot></slot>
</div>
</ElCard>
</div>
</div>
</template>

@ -0,0 +1,3 @@
import ContentWrap from './src/ContentWrap.vue'
export { ContentWrap }

@ -0,0 +1,32 @@
<script lang="ts" name="ContentWrap" setup>
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('content-wrap')
defineProps({
title: propTypes.string.def(''),
message: propTypes.string.def('')
})
</script>
<template>
<ElCard :class="[prefixCls, 'mb-15px']" shadow="never">
<template v-if="title" #header>
<div class="flex items-center">
<span class="text-16px font-700">{{ title }}</span>
<ElTooltip v-if="message" effect="dark" placement="right">
<template #content>
<div class="max-w-200px">{{ message }}</div>
</template>
<Icon :size="14" class="ml-5px" icon="ep:question-filled" />
</ElTooltip>
</div>
</template>
<div>
<slot></slot>
</div>
</ElCard>
</template>

@ -0,0 +1,3 @@
import CountTo from './src/CountTo.vue'
export { CountTo }

@ -0,0 +1,180 @@
<script lang="ts" name="CountTo" setup>
import { PropType } from 'vue'
import { isNumber } from '@/utils/is'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('count-to')
const props = defineProps({
startVal: propTypes.number.def(0),
endVal: propTypes.number.def(2021),
duration: propTypes.number.def(3000),
autoplay: propTypes.bool.def(true),
decimals: propTypes.number.validate((value: number) => value >= 0).def(0),
decimal: propTypes.string.def('.'),
separator: propTypes.string.def(','),
prefix: propTypes.string.def(''),
suffix: propTypes.string.def(''),
useEasing: propTypes.bool.def(true),
easingFn: {
type: Function as PropType<(t: number, b: number, c: number, d: number) => number>,
default(t: number, b: number, c: number, d: number) {
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b
}
}
})
const emit = defineEmits(['mounted', 'callback'])
const formatNumber = (num: number | string) => {
const { decimals, decimal, separator, suffix, prefix } = props
num = Number(num).toFixed(decimals)
num += ''
const x = num.split('.')
let x1 = x[0]
const x2 = x.length > 1 ? decimal + x[1] : ''
const rgx = /(\d+)(\d{3})/
if (separator && !isNumber(separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + separator + '$2')
}
}
return prefix + x1 + x2 + suffix
}
const state = reactive<{
localStartVal: number
printVal: number | null
displayValue: string
paused: boolean
localDuration: number | null
startTime: number | null
timestamp: number | null
rAF: any
remaining: number | null
}>({
localStartVal: props.startVal,
displayValue: formatNumber(props.startVal),
printVal: null,
paused: false,
localDuration: props.duration,
startTime: null,
timestamp: null,
remaining: null,
rAF: null
})
const displayValue = toRef(state, 'displayValue')
onMounted(() => {
if (props.autoplay) {
start()
}
emit('mounted')
})
const getCountDown = computed(() => {
return props.startVal > props.endVal
})
watch([() => props.startVal, () => props.endVal], () => {
if (props.autoplay) {
start()
}
})
const start = () => {
const { startVal, duration } = props
state.localStartVal = startVal
state.startTime = null
state.localDuration = duration
state.paused = false
state.rAF = requestAnimationFrame(count)
}
const pauseResume = () => {
if (state.paused) {
resume()
state.paused = false
} else {
pause()
state.paused = true
}
}
const pause = () => {
cancelAnimationFrame(state.rAF)
}
const resume = () => {
state.startTime = null
state.localDuration = +(state.remaining as number)
state.localStartVal = +(state.printVal as number)
requestAnimationFrame(count)
}
const reset = () => {
state.startTime = null
cancelAnimationFrame(state.rAF)
state.displayValue = formatNumber(props.startVal)
}
const count = (timestamp: number) => {
const { useEasing, easingFn, endVal } = props
if (!state.startTime) state.startTime = timestamp
state.timestamp = timestamp
const progress = timestamp - state.startTime
state.remaining = (state.localDuration as number) - progress
if (useEasing) {
if (unref(getCountDown)) {
state.printVal =
state.localStartVal -
easingFn(progress, 0, state.localStartVal - endVal, state.localDuration as number)
} else {
state.printVal = easingFn(
progress,
state.localStartVal,
endVal - state.localStartVal,
state.localDuration as number
)
}
} else {
if (unref(getCountDown)) {
state.printVal =
state.localStartVal -
(state.localStartVal - endVal) * (progress / (state.localDuration as number))
} else {
state.printVal =
state.localStartVal +
(endVal - state.localStartVal) * (progress / (state.localDuration as number))
}
}
if (unref(getCountDown)) {
state.printVal = state.printVal < endVal ? endVal : state.printVal
} else {
state.printVal = state.printVal > endVal ? endVal : state.printVal
}
state.displayValue = formatNumber(state.printVal!)
if (progress < (state.localDuration as number)) {
state.rAF = requestAnimationFrame(count)
} else {
emit('callback')
}
}
defineExpose({
pauseResume,
reset,
start,
pause
})
</script>
<template>
<span :class="prefixCls">
{{ displayValue }}
</span>
</template>

@ -0,0 +1,2 @@
import Crontab from './src/Crontab.vue'
export { Crontab }

File diff suppressed because it is too large Load Diff

@ -0,0 +1,4 @@
import CropperImage from './src/Cropper.vue'
import CropperAvatar from './src/CropperAvatar.vue'
export { CropperImage, CropperAvatar }

@ -0,0 +1,257 @@
<template>
<div>
<Dialog
v-model="dialogVisible"
:canFullscreen="false"
:title="t('cropper.modalTitle')"
maxHeight="380px"
width="800px"
>
<div :class="prefixCls">
<div :class="`${prefixCls}-left`">
<div :class="`${prefixCls}-cropper`">
<CropperImage
v-if="src"
:circled="circled"
:src="src"
height="300px"
@cropend="handleCropend"
@ready="handleReady"
/>
</div>
<div :class="`${prefixCls}-toolbar`">
<el-upload :beforeUpload="handleBeforeUpload" :fileList="[]" accept="image/*">
<el-tooltip :content="t('cropper.selectImage')" placement="bottom">
<XButton preIcon="ant-design:upload-outlined" type="primary" />
</el-tooltip>
</el-upload>
<el-space>
<el-tooltip :content="t('cropper.btn_reset')" placement="bottom">
<XButton
:disabled="!src"
preIcon="ant-design:reload-outlined"
size="small"
type="primary"
@click="handlerToolbar('reset')"
/>
</el-tooltip>
<el-tooltip :content="t('cropper.btn_rotate_left')" placement="bottom">
<XButton
:disabled="!src"
preIcon="ant-design:rotate-left-outlined"
size="small"
type="primary"
@click="handlerToolbar('rotate', -45)"
/>
</el-tooltip>
<el-tooltip :content="t('cropper.btn_rotate_right')" placement="bottom">
<XButton
:disabled="!src"
preIcon="ant-design:rotate-right-outlined"
size="small"
type="primary"
@click="handlerToolbar('rotate', 45)"
/>
</el-tooltip>
<el-tooltip :content="t('cropper.btn_scale_x')" placement="bottom">
<XButton
:disabled="!src"
preIcon="vaadin:arrows-long-h"
size="small"
type="primary"
@click="handlerToolbar('scaleX')"
/>
</el-tooltip>
<el-tooltip :content="t('cropper.btn_scale_y')" placement="bottom">
<XButton
:disabled="!src"
preIcon="vaadin:arrows-long-v"
size="small"
type="primary"
@click="handlerToolbar('scaleY')"
/>
</el-tooltip>
<el-tooltip :content="t('cropper.btn_zoom_in')" placement="bottom">
<XButton
:disabled="!src"
preIcon="ant-design:zoom-in-outlined"
size="small"
type="primary"
@click="handlerToolbar('zoom', 0.1)"
/>
</el-tooltip>
<el-tooltip :content="t('cropper.btn_zoom_out')" placement="bottom">
<XButton
:disabled="!src"
preIcon="ant-design:zoom-out-outlined"
size="small"
type="primary"
@click="handlerToolbar('zoom', -0.1)"
/>
</el-tooltip>
</el-space>
</div>
</div>
<div :class="`${prefixCls}-right`">
<div :class="`${prefixCls}-preview`">
<img v-if="previewSource" :alt="t('cropper.preview')" :src="previewSource" />
</div>
<template v-if="previewSource">
<div :class="`${prefixCls}-group`">
<el-avatar :src="previewSource" size="large" />
<el-avatar :size="48" :src="previewSource" />
<el-avatar :size="64" :src="previewSource" />
<el-avatar :size="80" :src="previewSource" />
</div>
</template>
</div>
</div>
<template #footer>
<el-button type="primary" @click="handleOk">{{ t('cropper.okText') }}</el-button>
</template>
</Dialog>
</div>
</template>
<script lang="ts" name="CopperModal" setup>
import { useDesign } from '@/hooks/web/useDesign'
import { dataURLtoBlob } from '@/utils/filt'
import { useI18n } from 'vue-i18n'
import type { CropendResult, Cropper } from './types'
import { propTypes } from '@/utils/propTypes'
import { CropperImage } from '@/components/Cropper'
const props = defineProps({
srcValue: propTypes.string.def(''),
circled: propTypes.bool.def(true)
})
const emit = defineEmits(['uploadSuccess'])
const { t } = useI18n()
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('cropper-am')
const src = ref(props.srcValue)
const previewSource = ref('')
const cropper = ref<Cropper>()
const dialogVisible = ref(false)
let filename = ''
let scaleX = 1
let scaleY = 1
// Block upload
function handleBeforeUpload(file: File) {
const reader = new FileReader()
reader.readAsDataURL(file)
src.value = ''
previewSource.value = ''
reader.onload = function (e) {
src.value = (e.target?.result as string) ?? ''
filename = file.name
}
return false
}
function handleCropend({ imgBase64 }: CropendResult) {
previewSource.value = imgBase64
}
function handleReady(cropperInstance: Cropper) {
cropper.value = cropperInstance
}
function handlerToolbar(event: string, arg?: number) {
if (event === 'scaleX') {
scaleX = arg = scaleX === -1 ? 1 : -1
}
if (event === 'scaleY') {
scaleY = arg = scaleY === -1 ? 1 : -1
}
cropper?.value?.[event]?.(arg)
}
async function handleOk() {
const blob = dataURLtoBlob(previewSource.value)
emit('uploadSuccess', { source: previewSource.value, data: blob, filename: filename })
}
function openModal() {
dialogVisible.value = true
}
function closeModal() {
dialogVisible.value = false
}
defineExpose({ openModal, closeModal })
</script>
<style lang="scss">
$prefix-cls: #{$namespace}-cropper-am;
.#{$prefix-cls} {
display: flex;
&-left,
&-right {
height: 340px;
}
&-left {
width: 55%;
}
&-right {
width: 45%;
}
&-cropper {
height: 300px;
background: #eee;
background-image: linear-gradient(
45deg,
rgb(0 0 0 / 25%) 25%,
transparent 0,
transparent 75%,
rgb(0 0 0 / 25%) 0
),
linear-gradient(
45deg,
rgb(0 0 0 / 25%) 25%,
transparent 0,
transparent 75%,
rgb(0 0 0 / 25%) 0
);
background-position: 0 0, 12px 12px;
background-size: 24px 24px;
}
&-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
&-preview {
width: 220px;
height: 220px;
margin: 0 auto;
overflow: hidden;
border: 1px solid;
border-radius: 50%;
img {
width: 100%;
height: 100%;
}
}
&-group {
display: flex;
padding-top: 8px;
margin-top: 8px;
border-top: 1px solid;
justify-content: space-around;
align-items: center;
}
}
</style>

@ -0,0 +1,181 @@
<template>
<div :class="getClass" :style="getWrapperStyle">
<img
v-show="isReady"
ref="imgElRef"
:alt="alt"
:crossorigin="crossorigin"
:src="src"
:style="getImageStyle"
/>
</div>
</template>
<script lang="ts" name="Cropper" setup>
import { CSSProperties, PropType } from 'vue'
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
import { useDesign } from '@/hooks/web/useDesign'
import { propTypes } from '@/utils/propTypes'
import { useDebounceFn } from '@vueuse/core'
type Options = Cropper.Options
const defaultOptions: Options = {
aspectRatio: 1,
zoomable: true,
zoomOnTouch: true,
zoomOnWheel: true,
cropBoxMovable: true,
cropBoxResizable: true,
toggleDragModeOnDblclick: true,
autoCrop: true,
background: true,
highlight: true,
center: true,
responsive: true,
restore: true,
checkCrossOrigin: true,
checkOrientation: true,
scalable: true,
modal: true,
guides: true,
movable: true,
rotatable: true
}
const props = defineProps({
src: propTypes.string.def(''),
alt: propTypes.string.def(''),
circled: propTypes.bool.def(false),
realTimePreview: propTypes.bool.def(true),
height: propTypes.string.def('360px'),
crossorigin: {
type: String as PropType<'' | 'anonymous' | 'use-credentials' | undefined>,
default: undefined
},
imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
options: { type: Object as PropType<Options>, default: () => ({}) }
})
const emit = defineEmits(['cropend', 'ready', 'cropendError'])
const attrs = useAttrs()
const imgElRef = ref<ElRef<HTMLImageElement>>()
const cropper = ref<Nullable<Cropper>>()
const isReady = ref(false)
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('cropper-image')
const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80)
const getImageStyle = computed((): CSSProperties => {
return {
height: props.height,
maxWidth: '100%',
...props.imageStyle
}
})
const getClass = computed(() => {
return [
prefixCls,
attrs.class,
{
[`${prefixCls}--circled`]: props.circled
}
]
})
const getWrapperStyle = computed((): CSSProperties => {
return { height: `${props.height}`.replace(/px/, '') + 'px' }
})
onMounted(init)
onUnmounted(() => {
cropper.value?.destroy()
})
async function init() {
const imgEl = unref(imgElRef)
if (!imgEl) {
return
}
cropper.value = new Cropper(imgEl, {
...defaultOptions,
ready: () => {
isReady.value = true
realTimeCroppered()
emit('ready', cropper.value)
},
crop() {
debounceRealTimeCroppered()
},
zoom() {
debounceRealTimeCroppered()
},
cropmove() {
debounceRealTimeCroppered()
},
...props.options
})
}
// Real-time display preview
function realTimeCroppered() {
props.realTimePreview && croppered()
}
// event: return base64 and width and height information after cropping
function croppered() {
if (!cropper.value) {
return
}
let imgInfo = cropper.value.getData()
const canvas = props.circled ? getRoundedCanvas() : cropper.value.getCroppedCanvas()
canvas.toBlob((blob) => {
if (!blob) {
return
}
let fileReader: FileReader = new FileReader()
fileReader.readAsDataURL(blob)
fileReader.onloadend = (e) => {
emit('cropend', {
imgBase64: e.target?.result ?? '',
imgInfo
})
}
fileReader.onerror = () => {
emit('cropendError')
}
}, 'image/png')
}
// Get a circular picture canvas
function getRoundedCanvas() {
const sourceCanvas = cropper.value!.getCroppedCanvas()
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')!
const width = sourceCanvas.width
const height = sourceCanvas.height
canvas.width = width
canvas.height = height
context.imageSmoothingEnabled = true
context.drawImage(sourceCanvas, 0, 0, width, height)
context.globalCompositeOperation = 'destination-in'
context.beginPath()
context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true)
context.fill()
return canvas
}
</script>
<style lang="scss">
$prefix-cls: #{$namespace}-cropper-image;
.#{$prefix-cls} {
&--circled {
.cropper-view-box,
.cropper-face {
border-radius: 50%;
}
}
}
</style>

@ -0,0 +1,141 @@
<template>
<div class="user-info-head" @click="open()">
<img v-if="sourceValue" :src="sourceValue" alt="avatar" class="img-circle img-lg" />
<el-button v-if="showBtn" :class="`${prefixCls}-upload-btn`" @click="open()">
{{ btnText ? btnText : t('cropper.selectImage') }}
</el-button>
<CopperModal
ref="cropperModelRef"
:srcValue="sourceValue"
@upload-success="handleUploadSuccess"
/>
</div>
</template>
<script lang="ts" name="CropperAvatar" setup>
import { useDesign } from '@/hooks/web/useDesign'
import { propTypes } from '@/utils/propTypes'
import { useI18n } from 'vue-i18n'
import CopperModal from './CopperModal.vue'
const props = defineProps({
width: propTypes.string.def('200px'),
value: propTypes.string.def(''),
showBtn: propTypes.bool.def(true),
btnText: propTypes.string.def('')
})
const emit = defineEmits(['update:value', 'change'])
const sourceValue = ref(props.value)
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('cropper-avatar')
const message = useMessage()
const { t } = useI18n()
const cropperModelRef = ref()
watchEffect(() => {
sourceValue.value = props.value
})
watch(
() => sourceValue.value,
(v: string) => {
emit('update:value', v)
}
)
function handleUploadSuccess({ source, data, filename }) {
sourceValue.value = source
emit('change', { source, data, filename })
message.success(t('cropper.uploadSuccess'))
}
function open() {
cropperModelRef.value.openModal()
}
function close() {
cropperModelRef.value.closeModal()
}
defineExpose({
open,
close
})
</script>
<style lang="scss" scoped>
$prefix-cls: #{$namespace}--cropper-avatar;
.#{$prefix-cls} {
display: inline-block;
text-align: center;
&-image-wrapper {
overflow: hidden;
cursor: pointer;
border: 1px solid;
border-radius: 50%;
img {
width: 100%;
}
}
&-image-mask {
opacity: 0%;
position: absolute;
width: inherit;
height: inherit;
border-radius: inherit;
border: inherit;
background: rgb(0 0 0 / 40%);
cursor: pointer;
transition: opacity 0.4s;
::v-deep(svg) {
margin: auto;
}
}
&-image-mask:hover {
opacity: 4000%;
}
&-upload-btn {
margin: 10px auto;
}
}
.user-info-head {
position: relative;
display: inline-block;
}
.img-circle {
border-radius: 50%;
}
.img-lg {
width: 120px;
height: 120px;
}
.user-info-head:hover:after {
content: '+';
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
color: #eee;
background: rgba(0, 0, 0, 0.5);
font-size: 24px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
cursor: pointer;
line-height: 110px;
border-radius: 50%;
}
</style>

@ -0,0 +1,8 @@
import type Cropper from 'cropperjs'
export interface CropendResult {
imgBase64: string
imgInfo: Cropper.Data
}
export type { Cropper }

@ -0,0 +1,3 @@
import Descriptions from './src/Descriptions.vue'
export { Descriptions }

@ -0,0 +1,172 @@
<script lang="ts" name="Descriptions" setup>
import { PropType } from 'vue'
import dayjs from 'dayjs'
import { useDesign } from '@/hooks/web/useDesign'
import { propTypes } from '@/utils/propTypes'
import { useAppStore } from '@/store/modules/app'
import { DescriptionsSchema } from '@/types/descriptions'
const appStore = useAppStore()
const mobile = computed(() => appStore.getMobile)
const attrs = useAttrs()
const slots = useSlots()
const props = defineProps({
title: propTypes.string.def(''),
message: propTypes.string.def(''),
collapse: propTypes.bool.def(true),
columns: propTypes.number.def(1),
labelWidth: propTypes.string.def('100px'),
schema: {
type: Array as PropType<DescriptionsSchema[]>,
default: () => []
},
data: {
type: Object as PropType<any>,
default: () => ({})
},
defaultShow: propTypes.bool.def(true)
})
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('descriptions')
const getBindValue = computed(() => {
const delArr: string[] = ['title', 'message', 'collapse', 'schema', 'data', 'class']
const obj = { ...attrs, ...props }
for (const key in obj) {
if (delArr.indexOf(key) !== -1) {
delete obj[key]
}
}
return obj
})
const getBindItemValue = (item: DescriptionsSchema) => {
const delArr: string[] = ['field']
const obj = { ...item }
for (const key in obj) {
if (delArr.indexOf(key) !== -1) {
delete obj[key]
}
}
return obj
}
//
const show = ref(props.defaultShow)
const toggleClick = () => {
if (props.collapse) {
show.value = !unref(show)
}
}
</script>
<template>
<div
:class="[
prefixCls,
'bg-[var(--el-color-white)] dark:(bg-[var(--el-bg-color)] border-[var(--el-border-color)] border-1px)'
]"
>
<div
v-if="title"
:class="[
`${prefixCls}-header`,
'h-40px flex justify-between items-center border-bottom-1 border-solid border-[var(--tags-view-border-color)] px-10px cursor-pointer dark:border-[var(--el-border-color)]'
]"
@click="toggleClick"
>
<div :class="[`${prefixCls}-header__title`, 'relative font-18px font-bold ml-10px']">
<div class="flex items-center">
{{ title }}
<ElTooltip v-if="message" :content="message" placement="right">
<Icon class="ml-5px" icon="ep:warning" />
</ElTooltip>
</div>
</div>
<Icon v-if="collapse" :icon="show ? 'ep:arrow-down' : 'ep:arrow-up'" />
</div>
<ElCollapseTransition>
<div v-show="show" :class="[`${prefixCls}-content`, 'p-10px']">
<ElDescriptions
:column="props.columns"
:direction="mobile ? 'vertical' : 'horizontal'"
border
v-bind="getBindValue"
>
<template v-if="slots['extra']" #extra>
<slot name="extra"></slot>
</template>
<ElDescriptionsItem
v-for="item in schema"
:key="item.field"
min-width="80"
:span="item.span"
label-class-name="desc-label"
v-bind="getBindItemValue(item)"
>
<template #label>
<slot
:name="`${item.field}-label`"
:row="{
label: item.label
}"
>{{ item.label }}
</slot>
</template>
<template #default>
<slot v-if="item.dateFormat">
{{
data[item.field] !== null ? dayjs(data[item.field]).format(item.dateFormat) : ''
}}
</slot>
<slot v-else-if="item.dictType">
<DictTag :type="item.dictType" :value="data[item.field] + ''" />
</slot>
<slot v-else-if="item.isEditor">
<div v-if="data[item.field]" v-dompurify-html="data[item.field]"></div>
</slot>
<slot v-else :name="item.field" :row="data">{{ data[item.field] }}</slot>
</template>
</ElDescriptionsItem>
</ElDescriptions>
</div>
</ElCollapseTransition>
</div>
</template>
<style lang="scss" scoped>
$prefix-cls: #{$namespace}-descriptions;
.#{$prefix-cls}-header {
&__title {
&::after {
position: absolute;
top: 3px;
left: -10px;
width: 4px;
height: 70%;
background: var(--el-color-primary);
content: '';
}
}
}
.#{$prefix-cls}-content {
// :deep(.#{$elNamespace}-descriptions__cell) {
// width: 0;
// }
:deep(.desc-label) {
width: v-bind('labelWidth');
}
}
</style>

@ -0,0 +1,3 @@
import Dialog from './src/Dialog.vue'
export { Dialog }

@ -0,0 +1,126 @@
<script lang="ts" name="Dialog" setup>
import { propTypes } from '@/utils/propTypes'
import { isNumber } from '@/utils/is'
const slots = useSlots()
const props = defineProps({
modelValue: propTypes.bool.def(false),
title: propTypes.string.def('Dialog'),
fullscreen: propTypes.bool.def(true),
width: propTypes.oneOfType([String, Number]).def('40%'),
scroll: propTypes.bool.def(false), // maxHeight
maxHeight: propTypes.oneOfType([String, Number]).def('300px')
})
const getBindValue = computed(() => {
const delArr: string[] = ['fullscreen', 'title', 'maxHeight']
const attrs = useAttrs()
const obj = { ...attrs, ...props }
for (const key in obj) {
if (delArr.indexOf(key) !== -1) {
delete obj[key]
}
}
return obj
})
const isFullscreen = ref(false)
const toggleFull = () => {
isFullscreen.value = !unref(isFullscreen)
}
const dialogHeight = ref(isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight)
watch(
() => isFullscreen.value,
async (val: boolean) => {
//
await nextTick()
if (val) {
const windowHeight = document.documentElement.offsetHeight
dialogHeight.value = `${windowHeight - 55 - 60 - (slots.footer ? 63 : 0)}px`
} else {
dialogHeight.value = isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight
}
},
{
immediate: true
}
)
const dialogStyle = computed(() => {
return {
height: unref(dialogHeight)
}
})
const emit = defineEmits(['close'])
</script>
<template>
<ElDialog
:close-on-click-modal="false"
:fullscreen="isFullscreen"
:width="width"
destroy-on-close
draggable
lock-scroll
v-bind="getBindValue"
@close="emit('close')"
>
<template #header>
<div class="flex justify-between">
<slot name="title">
{{ title }}
</slot>
<Icon
v-if="fullscreen"
:icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
class="mr-22px cursor-pointer is-hover mt-2px z-10"
color="var(--el-color-info)"
@click="toggleFull"
/>
</div>
</template>
<!-- 情况一如果 scroll true说明开启滚动条 -->
<ElScrollbar v-if="scroll" :style="dialogStyle">
<slot></slot>
</ElScrollbar>
<!-- 情况二如果 scroll false说明关闭滚动条滚动条 -->
<slot v-else></slot>
<template v-if="slots.footer" #footer>
<slot name="footer"></slot>
</template>
</ElDialog>
</template>
<style lang="scss">
.#{$elNamespace}-dialog__header {
margin-right: 0 !important;
border-bottom: 1px solid var(--tags-view-border-color);
}
.#{$elNamespace}-dialog__footer {
border-top: 1px solid var(--tags-view-border-color);
}
.is-hover {
&:hover {
color: var(--el-color-primary) !important;
}
}
.dark {
.#{$elNamespace}-dialog__header {
border-bottom: 1px solid var(--el-border-color);
}
.#{$elNamespace}-dialog__footer {
border-top: 1px solid var(--el-border-color);
}
}
</style>

@ -0,0 +1,3 @@
import DictTag from './src/DictTag.vue'
export { DictTag }

@ -0,0 +1,60 @@
<script lang="tsx">
import { defineComponent, PropType, ref } from 'vue'
import { isHexColor } from '@/utils/color'
import { ElTag } from 'element-plus'
import { DictDataType, getDictOptions } from '@/utils/dict'
export default defineComponent({
name: 'DictTag',
props: {
type: {
type: String as PropType<string>,
required: true
},
value: {
type: [String, Number, Boolean] as PropType<string | number | boolean>,
required: true
}
},
setup(props) {
const dictData = ref<DictDataType>()
const getDictObj = (dictType: string, value: string) => {
const dictOptions = getDictOptions(dictType)
dictOptions.forEach((dict: DictDataType) => {
if (dict.value === value) {
if (dict.colorType + '' === 'primary' || dict.colorType + '' === 'default') {
dict.colorType = ''
}
dictData.value = dict
}
})
}
const rederDictTag = () => {
if (!props.type) {
return null
}
//
if (props.value === undefined || props.value === null) {
return null
}
getDictObj(props.type, props.value.toString())
//
return (
<ElTag
style={dictData.value?.cssClass ? 'color: #fff' : ''}
type={dictData.value?.colorType}
color={
dictData.value?.cssClass && isHexColor(dictData.value?.cssClass)
? dictData.value?.cssClass
: ''
}
disableTransitions={true}
>
{dictData.value?.label}
</ElTag>
)
}
return () => rederDictTag()
}
})
</script>

@ -0,0 +1,32 @@
<template>
<el-alert v-if="getEnable()" type="success" show-icon>
<template #title>
<div @click="goToUrl">{{ '' + title + '文档地址' + url }}</div>
</template>
</el-alert>
</template>
<script setup lang="tsx" name="DocAlert">
import { propTypes } from '@/utils/propTypes'
const props = defineProps({
title: propTypes.string,
url: propTypes.string
})
/** 跳转 URL 链接 */
const goToUrl = () => {
window.open(props.url)
}
/** 是否开启 */
const getEnable = () => {
return import.meta.env.VITE_APP_TENANT_ENABLE === 'true'
}
</script>
<style scoped>
.el-alert--success.is-light {
border: 1px solid green;
margin-bottom: 10px;
cursor: pointer;
}
</style>

@ -0,0 +1,3 @@
import Echart from './src/Echart.vue'
export { Echart }

@ -0,0 +1,114 @@
<script lang="ts" name="EChart" setup>
import type { EChartsOption } from 'echarts'
import echarts from '@/plugins/echarts'
import { debounce } from 'lodash-es'
import 'echarts-wordcloud'
import { propTypes } from '@/utils/propTypes'
import { PropType } from 'vue'
import { useAppStore } from '@/store/modules/app'
import { isString } from '@/utils/is'
import { useDesign } from '@/hooks/web/useDesign'
const { getPrefixCls, variables } = useDesign()
const prefixCls = getPrefixCls('echart')
const appStore = useAppStore()
const props = defineProps({
options: {
type: Object as PropType<EChartsOption>,
required: true
},
width: propTypes.oneOfType([Number, String]).def(''),
height: propTypes.oneOfType([Number, String]).def('500px')
})
const isDark = computed(() => appStore.getIsDark)
const theme = computed(() => {
const echartTheme: boolean | string = unref(isDark) ? true : 'auto'
return echartTheme
})
const options = computed(() => {
return Object.assign(props.options, {
darkMode: unref(theme)
})
})
const elRef = ref<ElRef>()
let echartRef: Nullable<echarts.ECharts> = null
const contentEl = ref<Element>()
const styles = computed(() => {
const width = isString(props.width) ? props.width : `${props.width}px`
const height = isString(props.height) ? props.height : `${props.height}px`
return {
width,
height
}
})
const initChart = () => {
if (unref(elRef) && props.options) {
echartRef = echarts.init(unref(elRef) as HTMLElement)
echartRef?.setOption(unref(options))
}
}
watch(
() => options.value,
(options) => {
if (echartRef) {
echartRef?.setOption(options)
resizeHandler()
}
},
{
deep: true
}
)
const resizeHandler = debounce(() => {
if (echartRef) {
echartRef.resize()
}
}, 100)
const contentResizeHandler = async (e: TransitionEvent) => {
if (e.propertyName === 'width') {
resizeHandler()
}
}
onMounted(() => {
initChart()
window.addEventListener('resize', resizeHandler)
contentEl.value = document.getElementsByClassName(`${variables.namespace}-layout-content`)[0]
unref(contentEl) &&
(unref(contentEl) as Element).addEventListener('transitionend', contentResizeHandler)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeHandler)
unref(contentEl) &&
(unref(contentEl) as Element).removeEventListener('transitionend', contentResizeHandler)
})
onActivated(() => {
if (echartRef) {
echartRef.resize()
}
})
</script>
<template>
<div ref="elRef" :class="[$attrs.class, prefixCls]" :style="styles"></div>
</template>

@ -0,0 +1,8 @@
import Editor from './src/Editor.vue'
import { IDomEditor } from '@wangeditor/editor'
export interface EditorExpose {
getEditorRef: () => Promise<IDomEditor>
}
export { Editor }

@ -0,0 +1,280 @@
<script lang="ts" name="Editor" setup>
import { PropType } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { i18nChangeLanguage, IDomEditor, IEditorConfig } from '@wangeditor/editor'
import { propTypes } from '@/utils/propTypes'
import { isNumber } from '@/utils/is'
import { ElMessage } from 'element-plus'
import { useLocaleStore } from '@/store/modules/locale'
import { getAccessToken, getTenantId, getAppId } from '@/utils/auth'
type InsertFnType = (url: string, alt: string, href: string) => void
const localeStore = useLocaleStore()
const currentLocale = computed(() => localeStore.getCurrentLocale)
i18nChangeLanguage(unref(currentLocale).lang)
const props = defineProps({
editorId: propTypes.string.def('wangeEditor-1'),
height: propTypes.oneOfType([Number, String]).def('40vh'),
editorConfig: {
type: Object as PropType<Partial<IEditorConfig>>,
default: () => undefined
},
readonly: propTypes.bool.def(false),
modelValue: propTypes.string.def(''),
toolbarConfig: {
type: Object,
default: () => ({
excludeKeys: [
'insertVideo', //
'insertImage', //
'insertLink', //
'codeBlock', //
'headerSelect', //
'blockquote', //
'fontFamily', //
'todo', //
'group-indent', //
'emotion', //
'undo', //
'redo', //
'fullScreen'
]
})
}
})
const emit = defineEmits(['change', 'update:modelValue'])
// shallowRef
const editorRef = shallowRef<IDomEditor>()
const valueHtml = ref('')
watch(
() => props.modelValue,
(val: string) => {
if (val === unref(valueHtml)) return
valueHtml.value = val
},
{
immediate: true
}
)
//
watch(
() => valueHtml.value,
(val: string) => {
emit('update:modelValue', val)
}
)
const handleCreated = (editor: IDomEditor) => {
editorRef.value = editor
}
//
const editorConfig = computed((): IEditorConfig => {
return Object.assign(
{
placeholder: '请输入内容...',
readOnly: props.readonly,
customAlert: (s: string, t: string) => {
switch (t) {
case 'success':
ElMessage.success(s)
break
case 'info':
ElMessage.info(s)
break
case 'warning':
ElMessage.warning(s)
break
case 'error':
ElMessage.error(s)
break
default:
ElMessage.info(s)
break
}
},
autoFocus: false,
scroll: true,
MENU_CONF: {
['uploadImage']: {
server: import.meta.env.VITE_UPLOAD_URL,
// 2M
maxFileSize: 5 * 1024 * 1024,
// 100
maxNumberOfFiles: 10,
// ['image/*'] []
allowedFileTypes: ['image/*'],
// token formData
meta: { updateSupport: 0 },
// meta url false
metaWithUrl: true,
// http header
headers: {
Accept: '*',
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId(),
'instance-id': getAppId()
},
// cookie false
withCredentials: true,
// 10
timeout: 5 * 1000, // 5
// form-data fieldNamewangeditor-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 fieldNamewangeditor-uploaded-image
fieldName: 'file',
//
onBeforeUpload(file: File) {
console.log(file)
return file
},
//
onProgress(progress: number) {
// progress 0-100
console.log('progress', progress)
},
onSuccess(file: File, res: any) {
console.log('onSuccess', file, res)
},
onFailed(file: File, res: any) {
alert(res.message)
console.log('onFailed', file, res)
},
onError(file: File, err: any, res: any) {
alert(err.message)
console.error('onError', file, err, res)
},
//
customInsert(res: any, insertFn: InsertFnType) {
insertFn(res.data, 'video', res.data)
}
}
},
uploadImgShowBase64: true
},
props.editorConfig || {}
)
})
const editorStyle = computed(() => {
return {
height: isNumber(props.height) ? `${props.height}px` : props.height
}
})
//
const handleChange = (editor: IDomEditor) => {
emit('change', editor)
}
//
onBeforeUnmount(() => {
const editor = unref(editorRef.value)
if (editor === null) return
// editor
editor?.destroy()
})
const getEditorRef = async (): Promise<IDomEditor> => {
await nextTick()
return unref(editorRef.value) as IDomEditor
}
defineExpose({
getEditorRef
})
</script>
<template>
<div class="border-1 border-solid border-[var(--tags-view-border-color)] z-99">
<!-- 工具栏 -->
<Toolbar
:editor="editorRef"
:editorId="editorId"
:defaultConfig="toolbarConfig"
class="border-bottom-1 border-solid border-[var(--tags-view-border-color)]"
/>
<!-- 编辑器 -->
<Editor
v-model="valueHtml"
:defaultConfig="editorConfig"
:editorId="editorId"
:style="editorStyle"
@on-change="handleChange"
@on-created="handleCreated"
/>
</div>
</template>
<style src="@wangeditor/editor/dist/css/style.css"></style>

@ -0,0 +1,68 @@
**「工具栏key」**
[
"headerSelect",// 标题
"blockquote", // 引用
"bold", // 加粗
"underline", // 下划线
"italic", // 斜体
// 删除线、清除格式等
"group-more-style",
{
key: "group-more-style",
title: "更多",
iconSvg:
'<svg viewBox="0 0 1024 1024"><path d="M204.8 505.6…0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path></svg>',
menuKeys: Array(5)
},
"color", // 文字颜色
"bgColor", // 背景色
"fontSize", // 字号
"fontFamily", // 字体
"lineHeight", // 行高
"bulletedList", // 无序列表
"numberedList", // 有序列表
"todo", // 代办
// 对齐
"group-justify",
{
key: "group-justify",
title: "对齐",
iconSvg:
'<svg viewBox="0 0 1024 1024"><path d="M768 793.6v1…72.8 102.4v102.4H51.2V102.4h921.6z"></path></svg>',
menuKeys: Array(4)
},
// 缩进
"group-indent",
{
key: "group-indent",
title: "缩进",
iconSvg:
'<svg viewBox="0 0 1024 1024"><path d="M0 64h1024v1…32h1024v128H0z m0-128V320l256 192z"></path></svg>',
menuKeys: Array(2)
},
"emotion",// 表情
"insertLink",// 插入链接
"group-image",// 上传图片
{
key: "group-image",
title: "图片",
iconSvg:
'<svg viewBox="0 0 1024 1024"><path d="M959.877 128…l224.01-384 256 320h64l224.01-192z"></path></svg>',
menuKeys: Array(2)
},
"group-video",// 上传视频
{
key: "group-video",
title: "视频",
iconSvg:
'<svg viewBox="0 0 1024 1024"><path d="M981.184 160….904zM384 704V320l320 192-320 192z"></path></svg>',
menuKeys: Array(2)
},
"insertTable",// 插入表格
"codeBlock", // 代码块
"divider", // 分割线
"undo", // 撤销
"redo", // 重做
"fullScreen" // 全屏
]

@ -0,0 +1,3 @@
import Error from './src/Error.vue'
export { Error }

@ -0,0 +1,56 @@
<script lang="ts" name="Error" setup>
import pageError from '@/assets/svgs/404.svg'
import networkError from '@/assets/svgs/500.svg'
import noPermission from '@/assets/svgs/403.svg'
import { propTypes } from '@/utils/propTypes'
interface ErrorMap {
url: string
message: string
buttonText: string
}
const { t } = useI18n()
const errorMap: {
[key: string]: ErrorMap
} = {
'404': {
url: pageError,
message: t('error.pageError'),
buttonText: t('error.returnToHome')
},
'500': {
url: networkError,
message: t('error.networkError'),
buttonText: t('error.returnToHome')
},
'403': {
url: noPermission,
message: t('error.noPermission'),
buttonText: t('error.returnToHome')
}
}
const props = defineProps({
type: propTypes.string.validate((v: string) => ['404', '500', '403'].includes(v)).def('404')
})
const emit = defineEmits(['errorClick'])
const btnClick = () => {
emit('errorClick', props.type)
}
</script>
<template>
<div class="flex justify-center">
<div class="text-center">
<img :src="errorMap[type].url" alt="" width="350" />
<div class="text-14px text-[var(--el-color-info)]">{{ errorMap[type].message }}</div>
<div class="mt-20px">
<ElButton type="primary" @click="btnClick">{{ errorMap[type].buttonText }}</ElButton>
</div>
</div>
</div>
</template>

@ -0,0 +1,15 @@
import Form from './src/Form.vue'
import { ElForm } from 'element-plus'
import { FormSchema, FormSetPropsType } from '@/types/form'
export interface FormExpose {
setValues: (data: Recordable) => void
setProps: (props: Recordable) => void
delSchema: (field: string) => void
addSchema: (formSchema: FormSchema, index?: number) => void
setSchema: (schemaProps: FormSetPropsType[]) => void
formModel: Recordable
getElFormRef: () => ComponentRef<typeof ElForm>
}
export { Form }

@ -0,0 +1,314 @@
<script lang="tsx">
import { PropType, defineComponent, ref, computed, unref, watch, onMounted } from 'vue'
import { ElForm, ElFormItem, ElRow, ElCol, ElTooltip } from 'element-plus'
import { componentMap } from './componentMap'
import { propTypes } from '@/utils/propTypes'
import { getSlot } from '@/utils/tsxHelper'
import {
setTextPlaceholder,
setGridProp,
setComponentProps,
setItemComponentSlots,
initModel,
setFormItemSlots
} from './helper'
import { useRenderSelect } from './components/useRenderSelect'
import { useRenderRadio } from './components/useRenderRadio'
import { useRenderCheckbox } from './components/useRenderCheckbox'
import { useDesign } from '@/hooks/web/useDesign'
import { findIndex } from '@/utils'
import { set } from 'lodash-es'
import { FormProps } from './types'
import { Icon } from '@/components/Icon'
import { FormSchema, FormSetPropsType } from '@/types/form'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('form')
export default defineComponent({
name: 'Form',
props: {
// Form
schema: {
type: Array as PropType<FormSchema[]>,
default: () => []
},
//
// update by true false
isCol: propTypes.bool.def(false),
//
isSearch: propTypes.bool.def(false),
//
model: {
type: Object as PropType<Recordable>,
default: () => ({})
},
// placeholder
autoSetPlaceholder: propTypes.bool.def(true),
//
isCustom: propTypes.bool.def(false),
// label
labelWidth: propTypes.oneOfType([String, Number]).def('auto'),
// loading add by
vLoading: propTypes.bool.def(false),
inlineBlock: propTypes.bool.def(false)
},
emits: ['register'],
setup(props, { slots, expose, emit }) {
// element form
const elFormRef = ref<ComponentRef<typeof ElForm>>()
// useFormprops
const outsideProps = ref<FormProps>({})
const mergeProps = ref<FormProps>({})
const getProps = computed(() => {
const propsObj = { ...props }
Object.assign(propsObj, unref(mergeProps))
return propsObj
})
//
const formModel = ref<Recordable>({})
onMounted(() => {
emit('register', unref(elFormRef)?.$parent, unref(elFormRef))
})
//
const setValues = (data: Recordable = {}) => {
formModel.value = Object.assign(unref(formModel), data)
}
const setProps = (props: FormProps = {}) => {
mergeProps.value = Object.assign(unref(mergeProps), props)
outsideProps.value = props
}
const delSchema = (field: string) => {
const { schema } = unref(getProps)
const index = findIndex(schema, (v: FormSchema) => v.field === field)
if (index > -1) {
schema.splice(index, 1)
}
}
const addSchema = (formSchema: FormSchema, index?: number) => {
const { schema } = unref(getProps)
if (index !== void 0) {
schema.splice(index, 0, formSchema)
return
}
schema.push(formSchema)
}
const setSchema = (schemaProps: FormSetPropsType[]) => {
const { schema } = unref(getProps)
for (const v of schema) {
for (const item of schemaProps) {
if (v.field === item.field) {
set(v, item.path, item.value)
}
}
}
}
const getElFormRef = (): ComponentRef<typeof ElForm> => {
return unref(elFormRef) as ComponentRef<typeof ElForm>
}
expose({
setValues,
formModel,
setProps,
delSchema,
addSchema,
setSchema,
getElFormRef
})
// formModel
watch(
() => unref(getProps).schema,
(schema = []) => {
formModel.value = initModel(schema, unref(formModel))
},
{
immediate: true,
deep: true
}
)
// 使
const renderWrap = () => {
const { isCol } = unref(getProps)
const content = isCol ? (
<ElRow gutter={20}>{renderFormItemWrap()}</ElRow>
) : (
renderFormItemWrap()
)
return content
}
// el-col
const renderFormItemWrap = () => {
// hidden
const { schema = [], isCol } = unref(getProps)
return schema
.filter((v) => !v.hidden)
.map((item) => {
// Divider
const isDivider = item.component === 'Divider'
const Com = componentMap['Divider'] as ReturnType<typeof defineComponent>
return isDivider ? (
<Com {...{ contentPosition: 'left', ...item.componentProps }}>{item?.label}</Com>
) : isCol ? (
// ElCol
<ElCol {...setGridProp(item.colProps)}>{renderFormItem(item)}</ElCol>
) : (
renderFormItem(item)
)
})
}
// formItem
const renderFormItem = (item: FormSchema) => {
// options
const notRenderOptions = ['SelectV2', 'Cascader', 'Transfer']
const slotsMap: Recordable = {
...setItemComponentSlots(slots, item?.componentProps?.slots, item.field)
}
if (item?.component !== 'SelectV2' && item?.component !== 'Cascader' && item?.options) {
slotsMap.default = () => renderOptions(item)
}
const formItemSlots: Recordable = setFormItemSlots(slots, item.field)
// labelMessage使
if (item?.labelMessage) {
formItemSlots.label = () => {
return (
<>
<span>{item.label}</span>
<ElTooltip placement="right" raw-content>
{{
content: () => <span v-html={item.labelMessage}></span>,
default: () => (
<Icon
icon="ep:warning"
size={16}
color="var(--el-color-primary)"
class="ml-2px relative top-1px"
></Icon>
)
}}
</ElTooltip>
</>
)
}
}
const { isSearch } = unref(getProps)
return (
<ElFormItem
class={isSearch ? 'search-form-item' : 'crud-form-item'}
{...(item.formItemProps || {})}
prop={item.field}
label={item.label || ''}
>
{{
...formItemSlots,
default: () => {
const Com = componentMap[item.component as string] as ReturnType<
typeof defineComponent
>
const baseSty = isSearch ? '' : 'width: 100%;'
const { autoSetPlaceholder } = unref(getProps)
return slots[item.field] ? (
getSlot(slots, item.field, formModel.value)
) : (
<Com
vModel={formModel.value[item.field]}
{...(autoSetPlaceholder && setTextPlaceholder(item))}
{...setComponentProps(item)}
filterable
format={item.component == 'DatePicker' ? 'YYYY-MM-DD' : null}
value-format={item.component == 'DatePicker' ? 'YYYY-MM-DD' : null}
style={baseSty + item.componentProps?.style}
// eslint-disable-next-line prettier/prettier
{...(notRenderOptions.includes(item?.component as string) && item?.componentProps?.options
? { options: item?.componentProps?.options || [] }
: {})}
>
{{ ...slotsMap }}
</Com>
)
}
}}
</ElFormItem>
)
}
// options
const renderOptions = (item: FormSchema) => {
switch (item.component) {
case 'Select':
case 'SelectV2':
const { renderSelectOptions } = useRenderSelect(slots)
return renderSelectOptions(item)
case 'Radio':
case 'RadioButton':
const { renderRadioOptions } = useRenderRadio()
return renderRadioOptions(item)
case 'Checkbox':
case 'CheckboxButton':
const { renderCheckboxOptions } = useRenderCheckbox()
return renderCheckboxOptions(item)
default:
break
}
}
// Form
const getFormBindValue = () => {
//
const delKeys = ['schema', 'isCol', 'autoSetPlaceholder', 'isCustom', 'model']
const props = { ...unref(getProps) }
for (const key in props) {
if (delKeys.indexOf(key) !== -1) {
delete props[key]
}
}
return props
}
return () => (
<ElForm
ref={elFormRef}
{...getFormBindValue()}
model={props.isCustom ? props.model : formModel}
class={prefixCls}
v-loading={props.vLoading}
style={props.inlineBlock ? 'display: inline' : ''}
>
{{
//
default: () => {
const { isCustom } = unref(getProps)
return isCustom ? getSlot(slots, 'default') : renderWrap()
}
}}
</ElForm>
)
}
})
</script>
<style lang="scss" scoped>
.#{$elNamespace}-form.#{$namespace}-form .#{$elNamespace}-row {
margin-right: 0 !important;
margin-left: 0 !important;
}
</style>

@ -0,0 +1,55 @@
import type { Component } from 'vue'
import {
ElCascader,
ElCheckboxGroup,
ElColorPicker,
ElDatePicker,
ElInput,
ElInputNumber,
ElRadioGroup,
ElRate,
ElSelect,
ElSelectV2,
ElTreeSelect,
ElSlider,
ElSwitch,
ElTimePicker,
ElTimeSelect,
ElTransfer,
ElAutocomplete,
ElDivider
} from 'element-plus'
import { InputPassword } from '@/components/InputPassword'
import { Editor } from '@/components/Editor'
import { UploadImg, UploadImgs, UploadFile } from '@/components/UploadFile'
import { ComponentName } from '@/types/components'
const componentMap: Recordable<Component, ComponentName> = {
Radio: ElRadioGroup,
Checkbox: ElCheckboxGroup,
CheckboxButton: ElCheckboxGroup,
Input: ElInput,
Autocomplete: ElAutocomplete,
InputNumber: ElInputNumber,
Select: ElSelect,
Cascader: ElCascader,
Switch: ElSwitch,
Slider: ElSlider,
TimePicker: ElTimePicker,
DatePicker: ElDatePicker,
Rate: ElRate,
ColorPicker: ElColorPicker,
Transfer: ElTransfer,
Divider: ElDivider,
TimeSelect: ElTimeSelect,
SelectV2: ElSelectV2,
TreeSelect: ElTreeSelect,
RadioButton: ElRadioGroup,
InputPassword: InputPassword,
Editor: Editor,
UploadImg: UploadImg,
UploadImgs: UploadImgs,
UploadFile: UploadFile
}
export { componentMap }

@ -0,0 +1,26 @@
import { FormSchema } from '@/types/form'
import { ElCheckbox, ElCheckboxButton } from 'element-plus'
import { defineComponent } from 'vue'
export const useRenderCheckbox = () => {
const renderCheckboxOptions = (item: FormSchema) => {
// 如果有别名,就取别名
const labelAlias = item?.componentProps?.optionsAlias?.labelField || 'id'
const valueAlias = item?.componentProps?.optionsAlias?.valueField || 'name'
const Com = (item.component === 'Checkbox' ? ElCheckbox : ElCheckboxButton) as ReturnType<
typeof defineComponent
>
return item?.options?.map((option) => {
const { ...other } = option
return (
<Com {...other} label={option[valueAlias || 'value']}>
{option[labelAlias || 'label']}
</Com>
)
})
}
return {
renderCheckboxOptions
}
}

@ -0,0 +1,26 @@
import { FormSchema } from '@/types/form'
import { ElRadio, ElRadioButton } from 'element-plus'
import { defineComponent } from 'vue'
export const useRenderRadio = () => {
const renderRadioOptions = (item: FormSchema) => {
// 如果有别名,就取别名
const labelAlias = item?.componentProps?.optionsAlias?.labelField || 'id'
const valueAlias = item?.componentProps?.optionsAlias?.valueField || 'name'
const Com = (item.component === 'Radio' ? ElRadio : ElRadioButton) as ReturnType<
typeof defineComponent
>
return item?.options?.map((option) => {
const { ...other } = option
return (
<Com {...other} label={option[valueAlias || 'value']}>
{option[labelAlias || 'label']}
</Com>
)
})
}
return {
renderRadioOptions
}
}

@ -0,0 +1,57 @@
import { FormSchema } from '@/types/form'
import { ComponentOptions } from '@/types/components'
import { ElOption, ElOptionGroup } from 'element-plus'
import { getSlot } from '@/utils/tsxHelper'
import { Slots } from 'vue'
export const useRenderSelect = (slots: Slots) => {
// 渲染 select options
const renderSelectOptions = (item: FormSchema) => {
// 如果有别名,就取别名
const labelAlias = item?.componentProps?.optionsAlias?.labelField
return item?.options?.map((option) => {
if (option?.length) {
return (
<ElOptionGroup label={option[labelAlias || 'label']}>
{() => {
return option?.map((v) => {
return renderSelectOptionItem(item, v)
})
}}
</ElOptionGroup>
)
} else {
return renderSelectOptionItem(item, option)
}
})
}
// 渲染 select option item
const renderSelectOptionItem = (item: FormSchema, option: ComponentOptions) => {
// 如果有别名,就取别名
const labelAlias = item?.componentProps?.optionsAlias?.labelField || 'id'
const valueAlias = item?.componentProps?.optionsAlias?.valueField || 'name'
const { label, value, ...other } = option
return (
<ElOption
{...other}
label={labelAlias ? option[labelAlias] : label}
value={valueAlias ? option[valueAlias] : value}
>
{{
default: () =>
// option 插槽名规则,{field}-option
item?.componentProps?.optionsSlot
? getSlot(slots, `${item.field}-option`, { item: option })
: undefined
}}
</ElOption>
)
}
return {
renderSelectOptions
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save