feat: init
12
.env.development
Normal file
@ -0,0 +1,12 @@
|
||||
## 开发环境
|
||||
|
||||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||
NODE_ENV='development'
|
||||
|
||||
VITE_APP_PORT = 8090
|
||||
|
||||
# API请求前缀
|
||||
VITE_APP_BASE_API = '/api'
|
||||
|
||||
# proxy代理配置
|
||||
VITE_APP_API_URL = ' http://192.168.1.7:12500/'
|
||||
9
.env.production
Normal file
@ -0,0 +1,9 @@
|
||||
## 生产环境
|
||||
|
||||
VITE_APP_PORT = 3000
|
||||
|
||||
# API请求前缀
|
||||
VITE_APP_BASE_API = '/zsqy'
|
||||
|
||||
# proxy代理配置
|
||||
VITE_APP_API_URL = ""
|
||||
14
.eslintignore
Normal file
@ -0,0 +1,14 @@
|
||||
dist
|
||||
node_modules
|
||||
public
|
||||
.husky
|
||||
.vscode
|
||||
.idea
|
||||
*.sh
|
||||
*.md
|
||||
|
||||
src/assets
|
||||
|
||||
.eslintrc.cjs
|
||||
.prettierrc.cjs
|
||||
.stylelintrc.cjs
|
||||
284
.eslintrc-auto-import.json
Normal file
@ -0,0 +1,284 @@
|
||||
{
|
||||
"globals": {
|
||||
"Component": true,
|
||||
"ComponentPublicInstance": true,
|
||||
"ComputedRef": true,
|
||||
"EffectScope": true,
|
||||
"ElMessage": true,
|
||||
"ElMessageBox": true,
|
||||
"ElNotification": true,
|
||||
"InjectionKey": true,
|
||||
"PropType": true,
|
||||
"Ref": true,
|
||||
"VNode": 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,
|
||||
"createReusableTemplate": true,
|
||||
"createSharedComposable": true,
|
||||
"createTemplatePromise": 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,
|
||||
"toValue": true,
|
||||
"triggerRef": true,
|
||||
"tryOnBeforeMount": true,
|
||||
"tryOnBeforeUnmount": true,
|
||||
"tryOnMounted": true,
|
||||
"tryOnScopeDispose": true,
|
||||
"tryOnUnmounted": true,
|
||||
"unref": true,
|
||||
"unrefElement": true,
|
||||
"until": true,
|
||||
"useActiveElement": true,
|
||||
"useAnimate": true,
|
||||
"useArrayDifference": true,
|
||||
"useArrayEvery": true,
|
||||
"useArrayFilter": true,
|
||||
"useArrayFind": true,
|
||||
"useArrayFindIndex": true,
|
||||
"useArrayFindLast": true,
|
||||
"useArrayIncludes": true,
|
||||
"useArrayJoin": true,
|
||||
"useArrayMap": true,
|
||||
"useArrayReduce": true,
|
||||
"useArraySome": true,
|
||||
"useArrayUnique": true,
|
||||
"useAsyncQueue": true,
|
||||
"useAsyncState": true,
|
||||
"useAttrs": true,
|
||||
"useBase64": true,
|
||||
"useBattery": true,
|
||||
"useBluetooth": true,
|
||||
"useBreakpoints": true,
|
||||
"useBroadcastChannel": true,
|
||||
"useBrowserLocation": true,
|
||||
"useCached": true,
|
||||
"useClipboard": true,
|
||||
"useCloned": 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,
|
||||
"useParentElement": true,
|
||||
"usePerformanceObserver": true,
|
||||
"usePermission": true,
|
||||
"usePointer": true,
|
||||
"usePointerLock": true,
|
||||
"usePointerSwipe": true,
|
||||
"usePreferredColorScheme": true,
|
||||
"usePreferredContrast": true,
|
||||
"usePreferredDark": true,
|
||||
"usePreferredLanguages": true,
|
||||
"usePreferredReducedMotion": true,
|
||||
"usePrevious": true,
|
||||
"useRafFn": true,
|
||||
"useRefHistory": true,
|
||||
"useResizeObserver": true,
|
||||
"useScreenOrientation": true,
|
||||
"useScreenSafeArea": true,
|
||||
"useScriptTag": true,
|
||||
"useScroll": true,
|
||||
"useScrollLock": true,
|
||||
"useSessionStorage": true,
|
||||
"useShare": true,
|
||||
"useSlots": true,
|
||||
"useSorted": 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,
|
||||
"useToNumber": true,
|
||||
"useToString": 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,
|
||||
"watchDeep": true,
|
||||
"watchEffect": true,
|
||||
"watchIgnorable": true,
|
||||
"watchImmediate": true,
|
||||
"watchOnce": true,
|
||||
"watchPausable": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true,
|
||||
"watchThrottled": true,
|
||||
"watchTriggerable": true,
|
||||
"watchWithFilter": true,
|
||||
"whenever": true
|
||||
}
|
||||
}
|
||||
141
.eslintrc.cjs
Normal file
@ -0,0 +1,141 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
parser: "vue-eslint-parser",
|
||||
extends: [
|
||||
// https://eslint.vuejs.org/user-guide/#usage
|
||||
"plugin:vue/vue3-recommended",
|
||||
"./.eslintrc-auto-import.json",
|
||||
"prettier",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
parser: "@typescript-eslint/parser",
|
||||
project: "./tsconfig.*?.json",
|
||||
createDefaultProgram: false,
|
||||
extraFileExtensions: [".vue"],
|
||||
},
|
||||
plugins: ["vue", "@typescript-eslint"],
|
||||
rules: {
|
||||
// 禁止使用 var,用 let 和 const 代替
|
||||
"no-var": "error",
|
||||
|
||||
// 在 Vue 的 v-for 循环中要求使用 :key
|
||||
"vue/require-v-for-key": "error", // vue的for循环是否必须有key
|
||||
|
||||
// 关闭 Vue 组件名必须多字的规则
|
||||
"vue/multi-word-component-names": "off",
|
||||
|
||||
// 关闭 Vue 的 v-model 中的参数规则
|
||||
"vue/no-v-model-argument": "off",
|
||||
|
||||
// Vue 的 setup() 函数必须使用的变量规则
|
||||
"vue/script-setup-uses-vars": "error",
|
||||
|
||||
// 关闭 Vue 组件名的保留规则
|
||||
"vue/no-reserved-component-names": "off",
|
||||
|
||||
// 关闭 Vue 事件名的命名规则
|
||||
"vue/custom-event-name-casing": "off",
|
||||
|
||||
// 关闭 Vue 属性的顺序规则
|
||||
"vue/attributes-order": "off",
|
||||
|
||||
// 关闭一个文件只能有一个组件规则
|
||||
"vue/one-component-per-file": "off",
|
||||
|
||||
// 关闭 HTML 标签闭合换行规则
|
||||
"vue/html-closing-bracket-newline": "off",
|
||||
|
||||
// 关闭 HTML 属性每行最大数量规则
|
||||
"vue/max-attributes-per-line": "off",
|
||||
|
||||
// 关闭多行 HTML 元素内容的规则
|
||||
"vue/multiline-html-element-content-newline": "off",
|
||||
|
||||
// 关闭单行 HTML 元素内容的规则
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
|
||||
// 关闭 HTML 属性连字符的规则
|
||||
"vue/attribute-hyphenation": "off",
|
||||
|
||||
// 关闭要求 Vue 默认属性的规则
|
||||
"vue/require-default-prop": "off",
|
||||
|
||||
// 关闭要求 Vue 显式触发事件的规则
|
||||
"vue/require-explicit-emits": "off",
|
||||
|
||||
// HTML 标签自闭合规则
|
||||
"vue/html-self-closing": [
|
||||
"error",
|
||||
{
|
||||
html: {
|
||||
void: "always", // 自闭合标签必须自闭合
|
||||
normal: "never", // 普通标签不得自闭合
|
||||
component: "always", // 组件标签必须自闭合
|
||||
},
|
||||
svg: "always",
|
||||
math: "always",
|
||||
},
|
||||
],
|
||||
|
||||
// 警告空方法检查
|
||||
"@typescript-eslint/no-empty-function": "warn",
|
||||
|
||||
// 关闭 any 类型的警告
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
|
||||
// 关闭使用 @ts-ignore 的警告
|
||||
"@typescript-eslint/ban-ts-ignore": "off",
|
||||
|
||||
// 关闭使用 @ts-comment 的警告
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
|
||||
// 关闭一些特定类型的警告
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
|
||||
// 关闭函数返回类型的警告
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
|
||||
// 关闭 any 类型的警告
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
|
||||
// 关闭使用 require 的警告
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
|
||||
// 关闭使用尚未定义的变量的警告
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
|
||||
// 关闭模块导出类型的警告
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
|
||||
// 未使用的变量的警告
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
|
||||
// Prettier 配置
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
useTabs: false, // 不使用制表符
|
||||
},
|
||||
],
|
||||
},
|
||||
// eslint不能对html文件生效
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.html"],
|
||||
processor: "vue/.vue",
|
||||
},
|
||||
],
|
||||
// https://eslint.org/docs/latest/use/configure/language-options#specifying-globals
|
||||
globals: {
|
||||
OptionType: "readonly",
|
||||
},
|
||||
};
|
||||
19
.gitignore
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.vscode
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.local
|
||||
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
.vscode/extensions.json
|
||||
.vscode/settings.json
|
||||
4
.husky/commit-msg
Normal file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx --no-install commitlint --edit $1
|
||||
4
.husky/pre-commit
Normal file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint:lint-staged
|
||||
11
.prettierignore
Normal file
@ -0,0 +1,11 @@
|
||||
dist
|
||||
node_modules
|
||||
public
|
||||
.husky
|
||||
.vscode
|
||||
.idea
|
||||
*.sh
|
||||
*.md
|
||||
|
||||
src/assets
|
||||
stats.html
|
||||
46
.prettierrc.cjs
Normal file
@ -0,0 +1,46 @@
|
||||
module.exports = {
|
||||
// (x)=>{},单个参数箭头函数是否显示小括号。(always:始终显示;avoid:省略括号。默认:always)
|
||||
arrowParens: "always",
|
||||
// 开始标签的右尖括号是否跟随在最后一行属性末尾,默认false
|
||||
bracketSameLine: false,
|
||||
// 对象字面量的括号之间打印空格 (true - Example: { foo: bar } ; false - Example: {foo:bar})
|
||||
bracketSpacing: true,
|
||||
// 是否格式化一些文件中被嵌入的代码片段的风格(auto|off;默认auto)
|
||||
embeddedLanguageFormatting: "auto",
|
||||
// 指定 HTML 文件的空格敏感度 (css|strict|ignore;默认css)
|
||||
htmlWhitespaceSensitivity: "css",
|
||||
// 当文件已经被 Prettier 格式化之后,是否会在文件顶部插入一个特殊的 @format 标记,默认false
|
||||
insertPragma: false,
|
||||
// 在 JSX 中使用单引号替代双引号,默认false
|
||||
jsxSingleQuote: false,
|
||||
// 每行最多字符数量,超出换行(默认80)
|
||||
printWidth: 80,
|
||||
// 超出打印宽度 (always | never | preserve )
|
||||
proseWrap: "preserve",
|
||||
// 对象属性是否使用引号(as-needed | consistent | preserve;默认as-needed:对象的属性需要加引号才添加;)
|
||||
quoteProps: "as-needed",
|
||||
// 是否只格式化在文件顶部包含特定注释(@prettier| @format)的文件,默认false
|
||||
requirePragma: false,
|
||||
// 结尾添加分号
|
||||
semi: true,
|
||||
// 使用单引号 (true:单引号;false:双引号)
|
||||
singleQuote: false,
|
||||
// 缩进空格数,默认2个空格
|
||||
tabWidth: 2,
|
||||
// 元素末尾是否加逗号,默认es5: ES5中的 objects, arrays 等会添加逗号,TypeScript 中的 type 后不加逗号
|
||||
trailingComma: "es5",
|
||||
// 指定缩进方式,空格或tab,默认false,即使用空格
|
||||
useTabs: false,
|
||||
// vue 文件中是否缩进 <style> 和 <script> 标签,默认 false
|
||||
vueIndentScriptAndStyle: false,
|
||||
|
||||
endOfLine: "auto",
|
||||
overrides: [
|
||||
{
|
||||
files: "*.html",
|
||||
options: {
|
||||
parser: "html",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
11
.stylelintignore
Normal file
@ -0,0 +1,11 @@
|
||||
dist
|
||||
node_modules
|
||||
public
|
||||
.husky
|
||||
.vscode
|
||||
.idea
|
||||
*.sh
|
||||
*.md
|
||||
|
||||
src/assets
|
||||
stats.html
|
||||
43
.stylelintrc.cjs
Normal file
@ -0,0 +1,43 @@
|
||||
module.exports = {
|
||||
// 继承推荐规范配置
|
||||
extends: [
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-recommended-scss",
|
||||
"stylelint-config-recommended-vue/scss",
|
||||
"stylelint-config-html/vue",
|
||||
"stylelint-config-recess-order",
|
||||
],
|
||||
// 指定不同文件对应的解析器
|
||||
overrides: [
|
||||
{
|
||||
files: ["**/*.{vue,html}"],
|
||||
customSyntax: "postcss-html",
|
||||
},
|
||||
{
|
||||
files: ["**/*.{css,scss}"],
|
||||
customSyntax: "postcss-scss",
|
||||
},
|
||||
],
|
||||
// 自定义规则
|
||||
rules: {
|
||||
"import-notation": "string", // 指定导入CSS文件的方式("string"|"url")
|
||||
"selector-class-pattern": null, // 选择器类名命名规则
|
||||
"custom-property-pattern": null, // 自定义属性命名规则
|
||||
"keyframes-name-pattern": null, // 动画帧节点样式命名规则
|
||||
"no-descending-specificity": null, // 允许无降序特异性
|
||||
// 允许 global 、export 、deep伪类
|
||||
"selector-pseudo-class-no-unknown": [
|
||||
true,
|
||||
{
|
||||
ignorePseudoClasses: ["global", "export", "deep"],
|
||||
},
|
||||
],
|
||||
// 允许未知属性
|
||||
"property-no-unknown": [
|
||||
true,
|
||||
{
|
||||
ignoreProperties: ["menuBg", "menuText", "menuActiveText"],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
93
commitlint.config.cjs
Normal file
@ -0,0 +1,93 @@
|
||||
module.exports = {
|
||||
// 继承的规则
|
||||
extends: ["@commitlint/config-conventional"],
|
||||
// 自定义规则
|
||||
rules: {
|
||||
// @see https://commitlint.js.org/#/reference-rules
|
||||
|
||||
// 提交类型枚举,git提交type必须是以下类型
|
||||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"feat", // 新增功能
|
||||
"fix", // 修复缺陷
|
||||
"docs", // 文档变更
|
||||
"style", // 代码格式(不影响功能,例如空格、分号等格式修正)
|
||||
"refactor", // 代码重构(不包括 bug 修复、功能新增)
|
||||
"perf", // 性能优化
|
||||
"test", // 添加疏漏测试或已有测试改动
|
||||
"build", // 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)
|
||||
"ci", // 修改 CI 配置、脚本
|
||||
"revert", // 回滚 commit
|
||||
"chore", // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)
|
||||
],
|
||||
],
|
||||
"subject-case": [0], // subject大小写不做校验
|
||||
},
|
||||
|
||||
prompt: {
|
||||
messages: {
|
||||
type: "选择你要提交的类型 :",
|
||||
scope: "选择一个提交范围(可选):",
|
||||
customScope: "请输入自定义的提交范围 :",
|
||||
subject: "填写简短精炼的变更描述 :\n",
|
||||
body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
|
||||
breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
|
||||
footerPrefixesSelect: "选择关联issue前缀(可选):",
|
||||
customFooterPrefix: "输入自定义issue前缀 :",
|
||||
footer: "列举关联issue (可选) 例如: #31, #I3244 :\n",
|
||||
generatingByAI: "正在通过 AI 生成你的提交简短描述...",
|
||||
generatedSelectByAI: "选择一个 AI 生成的简短描述:",
|
||||
confirmCommit: "是否提交或修改commit ?",
|
||||
},
|
||||
// prettier-ignore
|
||||
types: [
|
||||
{ value: "feat", name: "特性: ✨ 新增功能", emoji: ":sparkles:" },
|
||||
{ value: "fix", name: "修复: 🐛 修复缺陷", emoji: ":bug:" },
|
||||
{ value: "docs", name: "文档: 📝 文档变更", emoji: ":memo:" },
|
||||
{ value: "style", name: "格式: 🌈 代码格式(不影响功能,例如空格、分号等格式修正)", emoji: ":lipstick:" },
|
||||
{ value: "refactor", name: "重构: 🔄 代码重构(不包括 bug 修复、功能新增)", emoji: ":recycle:" },
|
||||
{ value: "perf", name: "性能: 🚀 性能优化", emoji: ":zap:" },
|
||||
{ value: "test", name: "测试: 🧪 添加疏漏测试或已有测试改动", emoji: ":white_check_mark:"},
|
||||
{ value: "build", name: "构建: 📦️ 构建流程、外部依赖变更(如升级 npm 包、修改 vite 配置等)", emoji: ":package:"},
|
||||
{ value: "ci", name: "集成: ⚙️ 修改 CI 配置、脚本", emoji: ":ferris_wheel:"},
|
||||
{ value: "revert", name: "回退: ↩️ 回滚 commit",emoji: ":rewind:"},
|
||||
{ value: "chore", name: "其他: 🛠️ 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)", emoji: ":hammer:"},
|
||||
],
|
||||
useEmoji: true,
|
||||
emojiAlign: "center",
|
||||
useAI: false,
|
||||
aiNumber: 1,
|
||||
themeColorCode: "",
|
||||
scopes: [],
|
||||
allowCustomScopes: true,
|
||||
allowEmptyScopes: true,
|
||||
customScopesAlign: "bottom",
|
||||
customScopesAlias: "custom",
|
||||
emptyScopesAlias: "empty",
|
||||
upperCaseSubject: false,
|
||||
markBreakingChangeMode: false,
|
||||
allowBreakingChanges: ["feat", "fix"],
|
||||
breaklineNumber: 100,
|
||||
breaklineChar: "|",
|
||||
skipQuestions: [],
|
||||
issuePrefixes: [
|
||||
{ value: "closed", name: "closed: ISSUES has been processed" },
|
||||
],
|
||||
customIssuePrefixAlign: "top",
|
||||
emptyIssuePrefixAlias: "skip",
|
||||
customIssuePrefixAlias: "custom",
|
||||
allowCustomIssuePrefix: true,
|
||||
allowEmptyIssuePrefix: true,
|
||||
confirmColorize: true,
|
||||
maxHeaderLength: Infinity,
|
||||
maxSubjectLength: Infinity,
|
||||
minSubjectLength: 0,
|
||||
scopeOverrides: undefined,
|
||||
defaultBody: "",
|
||||
defaultIssues: "",
|
||||
defaultScope: "",
|
||||
defaultSubject: "",
|
||||
},
|
||||
};
|
||||
143
index.html
Normal file
@ -0,0 +1,143 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="" />
|
||||
<meta name="keywords" content="" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<title>中盛起元物联管理平台</title>
|
||||
<link rel="stylesheet" href="./font/index.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app" class="app">
|
||||
<style>
|
||||
.app-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.app-loading .app-loading-wrap {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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-top-color: transparent;
|
||||
border-right: 0;
|
||||
border-radius: 50%;
|
||||
animation: loader-inner 1s cubic-bezier(0.42, 0.61, 0.58, 0.41)
|
||||
infinite;
|
||||
}
|
||||
|
||||
@keyframes loader-outter {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loader-outter {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loader-inner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loader-inner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="app-loading">
|
||||
<div class="app-loading-wrap">
|
||||
<div class="app-loading-title">
|
||||
<img src="/logo.png" class="app-loading-logo" alt="Logo" />
|
||||
<div class="app-loading-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.ts"></script>
|
||||
<script>
|
||||
global = globalThis;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
110
package.json
Normal file
@ -0,0 +1,110 @@
|
||||
{
|
||||
"name": "iec104",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"dev": "vite serve --mode development",
|
||||
"dev:prod": "vite serve --mode production",
|
||||
"build:prod": "vite build --mode production && vue-tsc --noEmit",
|
||||
"prepare": "husky install",
|
||||
"lint:eslint": "eslint --fix --ext .ts,.js,.vue ./src ",
|
||||
"lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}\"",
|
||||
"lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --fix",
|
||||
"lint:lint-staged": "lint-staged",
|
||||
"commit": "git-cz"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "node_modules/cz-git"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{cjs,json}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{vue,html}": [
|
||||
"eslint --fix",
|
||||
"prettier --write",
|
||||
"stylelint --fix"
|
||||
],
|
||||
"*.{scss,css}": [
|
||||
"stylelint --fix --allow-empty-input",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.md": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@vicons/antd": "^0.12.0",
|
||||
"@vicons/ionicons5": "^0.12.0",
|
||||
"@vitejs/plugin-vue": "^4.4.0",
|
||||
"@vueuse/core": "^10.5.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "5.1.10",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"axios": "^1.6.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"echarts": "^5.4.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"naive-ui": "^2.36.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"pinia": "^2.1.7",
|
||||
"terser": "^5.24.0",
|
||||
"vue": "^3.3.8",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue-types": "^5.1.1",
|
||||
"vue3-seamless-scroll": "^2.0.1",
|
||||
"vuedraggable": "^2.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.8.1",
|
||||
"@commitlint/config-conventional": "^17.8.1",
|
||||
"@iconify-json/ep": "^1.1.12",
|
||||
"@types/lodash": "^4.14.201",
|
||||
"@types/path-browserify": "^1.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||
"@typescript-eslint/parser": "^6.12.0",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.2",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"commitizen": "^4.3.0",
|
||||
"cz-git": "^1.7.1",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.18.1",
|
||||
"fast-glob": "^3.3.2",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.3.0",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-px-to-viewport": "^1.1.1",
|
||||
"postcss-scss": "^4.0.9",
|
||||
"prettier": "^2.8.8",
|
||||
"sass": "^1.69.5",
|
||||
"stylelint": "^15.11.0",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-recess-order": "^4.3.0",
|
||||
"stylelint-config-recommended-scss": "^13.1.0",
|
||||
"stylelint-config-recommended-vue": "^1.5.0",
|
||||
"stylelint-config-standard": "^34.0.0",
|
||||
"stylelint-config-standard-scss": "^11.1.0",
|
||||
"typescript": "^5.3.2",
|
||||
"unplugin-auto-import": "^0.15.3",
|
||||
"unplugin-icons": "^0.16.6",
|
||||
"unplugin-vue-components": "^0.24.1",
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vue-tsc": "^1.8.22"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
}
|
||||
20
postcss.config.js
Normal file
@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}, //// 用来给不同的浏览器自动添加相应前缀,如-webkit-,-moz-等等
|
||||
"postcss-px-to-viewport": {
|
||||
unitToConvert: "px", // 要转化的单位
|
||||
viewportWidth: 1920, // UI设计稿的宽度
|
||||
viewportHeight: 1080,
|
||||
unitPrecision: 6, // 转换后的精度,即小数点位数
|
||||
propList: ["*"], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
|
||||
viewportUnit: "vw", // 指定需要转换成的视窗单位,默认vw
|
||||
fontViewportUnit: "vw", // 指定字体需要转换成的视窗单位,默认vw
|
||||
selectorBlackList: [".ignore"], // 指定不转换为视窗单位的类名,
|
||||
minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
|
||||
mediaQuery: false, // 是否在媒体查询的css代码中也进行转换,默认false
|
||||
replace: true, // 是否转换后直接更换属性值
|
||||
exclude: /(\/|\\)(node_modules)(\/|\\)/, // 设置忽略文件,用正则做目录名匹配
|
||||
// landscape: true // 是否处理横屏情况,
|
||||
},
|
||||
},
|
||||
};
|
||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
public/font/PingFang.otf
Normal file
BIN
public/font/SourceHanSansCN-Light.otf
Normal file
BIN
public/font/SourceHanSansK-Regular.ttf
Normal file
BIN
public/font/YouSheBiaoTiHei-2.ttf
Normal file
27
public/font/index.css
Normal file
@ -0,0 +1,27 @@
|
||||
@font-face {
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
src: url("./YouSheBiaoTiHei-2.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: SourceHanSansCN;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
src: url("./SourceHanSansCN-Light.otf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: SourceHanSansK;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
src: url("./SourceHanSansK-Regular.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: PingFang;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
src: url("./PingFang.otf");
|
||||
}
|
||||
BIN
public/logo.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
public/logo2.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
10
src/App.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { NConfigProvider } from "naive-ui";
|
||||
import { zhCN, dateZhCN } from "naive-ui";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider :locale="zhCN" :date-locale="dateZhCN">
|
||||
<router-view />
|
||||
</n-config-provider>
|
||||
</template>
|
||||
48
src/api/axios.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import axios, { AxiosResponse, AxiosRequestConfig } from "axios";
|
||||
import { ResultEnum } from "@/enums/httpEnum";
|
||||
// import { ErrorPageNameMap } from "@/enums/pageEnum";
|
||||
// import { redirectErrorPage } from "@/utils";
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: import.meta.env.DEV
|
||||
? import.meta.env.VITE_APP_API_URL
|
||||
: `http://${window.location.host}/`,
|
||||
timeout: ResultEnum.TIMEOUT,
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
axiosInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
return config;
|
||||
},
|
||||
(error: AxiosRequestConfig) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
axiosInstance.interceptors.response.use(
|
||||
(res: AxiosResponse) => {
|
||||
const { message, success } = res.data as {
|
||||
success: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
// 如果是文件流,直接过
|
||||
if (res.config.responseType === "blob") return Promise.resolve(res.data);
|
||||
if (success) return Promise.resolve(res.data);
|
||||
// 如果 success 为 false,显示服务器返回的错误消息
|
||||
window["$message"].error(message || "系统错误");
|
||||
|
||||
// 重定向
|
||||
// if (ErrorPageNameMap.get(status)) redirectErrorPage(status);
|
||||
return Promise.resolve(res.data);
|
||||
},
|
||||
(err: AxiosResponse) => {
|
||||
window["$message"].error("接口异常,请检查");
|
||||
return Promise.reject(err);
|
||||
}
|
||||
);
|
||||
|
||||
// 导出 axios 实例
|
||||
export default axiosInstance;
|
||||
221
src/api/http.ts
Normal file
@ -0,0 +1,221 @@
|
||||
import axiosInstance from "./axios";
|
||||
import type {
|
||||
RequestGlobalConfigType,
|
||||
RequestConfigType,
|
||||
} from "@/enums/httpEnum";
|
||||
import {
|
||||
RequestHttpEnum,
|
||||
ContentTypeEnum,
|
||||
RequestBodyEnum,
|
||||
RequestDataTypeEnum,
|
||||
RequestContentTypeEnum,
|
||||
RequestParamsObjType,
|
||||
} from "@/enums/httpEnum";
|
||||
|
||||
export const get = (url: string, params?: object) => {
|
||||
return axiosInstance({
|
||||
url: url,
|
||||
method: RequestHttpEnum.GET,
|
||||
params: params,
|
||||
headers: {
|
||||
Authorization: "7df1bd95-bf13-4660-a07d-90b8b1b314e8",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const post = (url: string, data?: object, headersType?: string) => {
|
||||
return axiosInstance({
|
||||
url: url,
|
||||
method: RequestHttpEnum.POST,
|
||||
data: data,
|
||||
headers: {
|
||||
"Content-Type": headersType || ContentTypeEnum.JSON,
|
||||
Authorization: "7df1bd95-bf13-4660-a07d-90b8b1b314e8",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const patch = (url: string, data?: object, headersType?: string) => {
|
||||
return axiosInstance({
|
||||
url: url,
|
||||
method: RequestHttpEnum.PATCH,
|
||||
data: data,
|
||||
headers: {
|
||||
"Content-Type": headersType || ContentTypeEnum.JSON,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const put = (
|
||||
url: string,
|
||||
data?: object,
|
||||
headersType?: ContentTypeEnum
|
||||
) => {
|
||||
return axiosInstance({
|
||||
url: url,
|
||||
method: RequestHttpEnum.PUT,
|
||||
data: data,
|
||||
headers: {
|
||||
"Content-Type": headersType || ContentTypeEnum.JSON,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const del = (url: string, params?: object) => {
|
||||
return axiosInstance({
|
||||
url: url,
|
||||
method: RequestHttpEnum.DELETE,
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
// 获取请求函数,默认get
|
||||
export const http = (type?: RequestHttpEnum) => {
|
||||
switch (type) {
|
||||
case RequestHttpEnum.GET:
|
||||
return get;
|
||||
|
||||
case RequestHttpEnum.POST:
|
||||
return post;
|
||||
|
||||
case RequestHttpEnum.PATCH:
|
||||
return patch;
|
||||
|
||||
case RequestHttpEnum.PUT:
|
||||
return put;
|
||||
|
||||
case RequestHttpEnum.DELETE:
|
||||
return del;
|
||||
|
||||
default:
|
||||
return get;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* * 自定义请求
|
||||
* @param targetParams 当前组件参数
|
||||
* @param globalParams 全局参数
|
||||
*/
|
||||
export const customizeHttp = (
|
||||
targetParams: RequestConfigType,
|
||||
globalParams: RequestGlobalConfigType
|
||||
) => {
|
||||
if (!targetParams || !globalParams) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 全局
|
||||
const {
|
||||
// 全局请求源地址
|
||||
requestOriginUrl,
|
||||
// 全局请求内容
|
||||
requestParams: globalRequestParams,
|
||||
} = globalParams;
|
||||
|
||||
// 目标组件(优先级 > 全局组件)
|
||||
const {
|
||||
// requestInterval,
|
||||
// 请求地址
|
||||
requestUrl,
|
||||
// 普通 / sql
|
||||
requestContentType,
|
||||
// 获取数据的方式
|
||||
requestDataType,
|
||||
// 请求方式 get/post/del/put/patch
|
||||
requestHttpType,
|
||||
// 请求体类型 none / form-data / x-www-form-urlencoded / json /xml
|
||||
requestParamsBodyType,
|
||||
// SQL 请求对象
|
||||
requestSQLContent,
|
||||
// 请求内容 params / cookie / header / body: 同 requestParamsBodyType
|
||||
requestParams: targetRequestParams,
|
||||
} = targetParams;
|
||||
|
||||
// 静态排除
|
||||
if (requestDataType === RequestDataTypeEnum.STATIC) return;
|
||||
|
||||
if (!requestUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理头部
|
||||
const headers: RequestParamsObjType = {
|
||||
...globalRequestParams.Header,
|
||||
...targetRequestParams.Header,
|
||||
};
|
||||
|
||||
// data 参数
|
||||
let data: RequestParamsObjType | FormData | string = {};
|
||||
// params 参数
|
||||
const params: RequestParamsObjType = targetRequestParams.Params;
|
||||
// form 类型处理
|
||||
const formData: FormData = new FormData();
|
||||
formData.set("default", "defaultData");
|
||||
// 类型处理
|
||||
|
||||
switch (requestParamsBodyType) {
|
||||
case RequestBodyEnum.NONE:
|
||||
break;
|
||||
|
||||
case RequestBodyEnum.JSON:
|
||||
headers["Content-Type"] = ContentTypeEnum.JSON;
|
||||
data = JSON.parse(targetRequestParams.Body["json"]);
|
||||
// json 赋值给 data
|
||||
break;
|
||||
|
||||
case RequestBodyEnum.XML:
|
||||
headers["Content-Type"] = ContentTypeEnum.XML;
|
||||
// xml 字符串赋值给 data
|
||||
data = targetRequestParams.Body["xml"];
|
||||
break;
|
||||
|
||||
case RequestBodyEnum.X_WWW_FORM_URLENCODED:
|
||||
headers["Content-Type"] = ContentTypeEnum.FORM_URLENCODED;
|
||||
const bodyFormData = targetRequestParams.Body["x-www-form-urlencoded"];
|
||||
for (const i in bodyFormData) formData.set(i, bodyFormData[i]);
|
||||
// FormData 赋值给 data
|
||||
data = formData;
|
||||
break;
|
||||
|
||||
case RequestBodyEnum.FORM_DATA:
|
||||
headers["Content-Type"] = ContentTypeEnum.FORM_DATA;
|
||||
const bodyFormUrlencoded = targetRequestParams.Body["form-data"];
|
||||
for (const i in bodyFormUrlencoded) {
|
||||
formData.set(i, bodyFormUrlencoded[i]);
|
||||
}
|
||||
// FormData 赋值给 data
|
||||
data = formData;
|
||||
break;
|
||||
}
|
||||
|
||||
// sql 处理
|
||||
if (requestContentType === RequestContentTypeEnum.SQL) {
|
||||
headers["Content-Type"] = ContentTypeEnum.JSON;
|
||||
data = requestSQLContent;
|
||||
}
|
||||
// 如果定义了 requestInterval 且请求次数未达到某个上限,则使用 setInterval 实现轮询
|
||||
// let requestCount = 0;
|
||||
// if (requestInterval && requestCount < 1) {
|
||||
// const intervalId = setInterval(() => {
|
||||
// // 在此处调用 customizeHttp 以实现轮询
|
||||
// customizeHttp(targetParams, globalParams);
|
||||
|
||||
// // 增加请求计数
|
||||
// requestCount++;
|
||||
|
||||
// // 如果请求次数达到上限,清除定时器
|
||||
// if (requestCount >= 1) {
|
||||
// clearInterval(intervalId);
|
||||
// }
|
||||
// }, requestInterval * 1000);
|
||||
// }
|
||||
|
||||
return axiosInstance({
|
||||
url: `${requestOriginUrl}${requestUrl}`,
|
||||
method: requestHttpType,
|
||||
data,
|
||||
params,
|
||||
headers,
|
||||
});
|
||||
};
|
||||
1
src/api/system/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./menu";
|
||||
16
src/api/system/menu.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { get, post } from "@/api/http";
|
||||
|
||||
const fix = "/menu";
|
||||
|
||||
const url = {
|
||||
insert: `${fix}/insert`, // 分页查询
|
||||
tree: `${fix}/tree`, // 查询菜单
|
||||
};
|
||||
|
||||
export const insertMenu = (params: Object) => {
|
||||
return post(url.insert, params);
|
||||
};
|
||||
|
||||
export const getTree = () => {
|
||||
return get(url.tree);
|
||||
};
|
||||
BIN
src/assets/images/401.gif
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
src/assets/images/404.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
src/assets/images/404_cloud.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src/assets/images/account-logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/images/active.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src/assets/images/camera1.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
src/assets/images/camera2.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
src/assets/images/chart.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
src/assets/images/header.png
Normal file
|
After Width: | Height: | Size: 366 KiB |
BIN
src/assets/images/inactive.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
127
src/assets/images/login.svg
Normal file
@ -0,0 +1,127 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1361px"
|
||||
height="609px" viewBox="0 0 1361 609" version="1.1">
|
||||
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Group 21</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs/>
|
||||
<g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
|
||||
<g id="Group-21" transform="translate(77.000000, 73.000000)">
|
||||
<g id="Group-18" opacity="0.8"
|
||||
transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
|
||||
<ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367"
|
||||
rx="21.7830479" ry="21.766008"/>
|
||||
<ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601"
|
||||
rx="5.2173913" ry="5.21330997"/>
|
||||
<path
|
||||
d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z"
|
||||
id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"/>
|
||||
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6"
|
||||
stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7"
|
||||
stroke-width="0.702678964" opacity="0.7" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"/>
|
||||
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9"
|
||||
stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-dasharray="1.405357899873153,2.108036953469981"/>
|
||||
<g id="Group-17"
|
||||
transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)"
|
||||
fill="#CFDAE6">
|
||||
<ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653"
|
||||
ry="9.12768076"/>
|
||||
<path
|
||||
d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z"
|
||||
id="Oval-4"
|
||||
transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Group-14"
|
||||
transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
|
||||
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439"
|
||||
rx="29.1176471" ry="29.1402439"/>
|
||||
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439"
|
||||
rx="21.5686275" ry="21.5853659"/>
|
||||
<ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341"
|
||||
rx="23.7254902" ry="23.7439024"/>
|
||||
<ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439"
|
||||
rx="10.7843137" ry="10.7926829"/>
|
||||
<path
|
||||
d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z"
|
||||
id="Oval-2" fill="#BACAD9"/>
|
||||
<g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)"
|
||||
fill="#E6A1A6">
|
||||
<ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824"
|
||||
ry="6.47560976"/>
|
||||
<path
|
||||
d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z"
|
||||
id="Oval-2-Copy-2"
|
||||
transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "/>
|
||||
</g>
|
||||
<ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706"
|
||||
ry="1.61890244"/>
|
||||
<ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098"
|
||||
rx="1.61764706" ry="1.61890244"/>
|
||||
<ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488"
|
||||
rx="2.15686275" ry="2.15853659"/>
|
||||
<path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6"
|
||||
opacity="0.8"/>
|
||||
</g>
|
||||
<g id="Group-10" opacity="0.799999952"
|
||||
transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
|
||||
<ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32"
|
||||
rx="11.1864407" ry="11.2941176"/>
|
||||
<g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
|
||||
<ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627"
|
||||
ry="8.55614973"/>
|
||||
<path
|
||||
d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z"
|
||||
id="Oval-7"/>
|
||||
</g>
|
||||
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6"
|
||||
stroke-width="0.941176471"/>
|
||||
<ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186"
|
||||
cy="3.29411765" rx="3.26271186" ry="3.29411765"/>
|
||||
<ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017"
|
||||
ry="2.82352941"/>
|
||||
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6"
|
||||
stroke-width="0.941176471"/>
|
||||
</g>
|
||||
<g id="Group-19" opacity="0.33"
|
||||
transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
|
||||
<g id="Group-17"
|
||||
transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)"
|
||||
fill="#BACAD9">
|
||||
<circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"/>
|
||||
<path
|
||||
d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z"
|
||||
id="Oval-4"
|
||||
transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "/>
|
||||
</g>
|
||||
<circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"/>
|
||||
<path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9"
|
||||
stroke-width="1.16666667"/>
|
||||
<path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9"
|
||||
stroke-width="1.16666667"/>
|
||||
<polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667"
|
||||
points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"/>
|
||||
<path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7"
|
||||
stroke-width="1.16666667" opacity="0.6"/>
|
||||
<path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9"
|
||||
stroke-width="1.16666667"/>
|
||||
<path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6"
|
||||
stroke-width="1.16666667"/>
|
||||
<circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"/>
|
||||
<circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"/>
|
||||
<circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25"
|
||||
r="8.75"/>
|
||||
<circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333"
|
||||
cy="30.3333333" r="5.83333333"/>
|
||||
<circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<div xmlns="" id="divScriptsUsed" style="display: none"/>
|
||||
<script xmlns="" id="globalVarsDetection"
|
||||
src="chrome-extension://cmkdbmfndkfgebldhnkbfhlneefdaaip/js/wrs_env.js"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/images/logo.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
src/assets/images/ring.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
src/assets/images/sy_user.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/images/table1.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/assets/images/table2.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/assets/images/title.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
src/assets/images/video1.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/assets/images/video2.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/images/video3.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
src/assets/images/video4.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
1
src/assets/svgs/403.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
1
src/assets/svgs/404.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
1
src/assets/svgs/500.svg
Normal file
|
After Width: | Height: | Size: 19 KiB |
155
src/components/Charts/Histogram.vue
Normal file
@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: "histogram",
|
||||
},
|
||||
});
|
||||
|
||||
let xLabel = ["09/01", "09/02", "09/03", "09/04", "09/05"];
|
||||
|
||||
let yLabel = [20, 80, 100, 40, 34, 90, 60];
|
||||
|
||||
const options = {
|
||||
// animation: false,
|
||||
grid: {
|
||||
top: "18%",
|
||||
bottom: "20%", //也可设置left和right设置距离来控制图表的大小
|
||||
},
|
||||
xAxis: {
|
||||
data: xLabel,
|
||||
axisLine: {
|
||||
show: true, //隐藏X轴轴线
|
||||
lineStyle: {
|
||||
color: "#11417a",
|
||||
},
|
||||
},
|
||||
axisTick: {
|
||||
show: false, //隐藏X轴刻度
|
||||
},
|
||||
axisLabel: {
|
||||
show: true,
|
||||
// margin: 14,
|
||||
fontSize: 12,
|
||||
textStyle: {
|
||||
color: "#fff", //X轴文字颜色
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: "value",
|
||||
gridIndex: 0,
|
||||
min: 0,
|
||||
max: 100,
|
||||
interval: 25,
|
||||
// splitNumber: 4,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "rgba(255, 255, 255, .2)",
|
||||
type: "dashed",
|
||||
},
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "#63273242",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
show: true,
|
||||
// margin: 14,
|
||||
fontSize: 12,
|
||||
textStyle: {
|
||||
color: "#fff", //X轴文字颜色
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: "",
|
||||
type: "bar",
|
||||
barWidth: 20,
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 1,
|
||||
color: "rgba(79, 203, 255, 1)",
|
||||
},
|
||||
{
|
||||
offset: 0,
|
||||
color: "rgba(140, 225, 251,1)",
|
||||
},
|
||||
]),
|
||||
},
|
||||
},
|
||||
data: yLabel,
|
||||
z: 10,
|
||||
zlevel: 0,
|
||||
label: {
|
||||
show: true,
|
||||
position: "top",
|
||||
// distance: 10,
|
||||
fontSize: 12,
|
||||
color: "#fff",
|
||||
// backgroundColor: "#0f375f",
|
||||
},
|
||||
},
|
||||
{
|
||||
// 分隔
|
||||
type: "pictorialBar",
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: "#0F375F",
|
||||
},
|
||||
},
|
||||
symbolRepeat: "fixed",
|
||||
symbolMargin: 6,
|
||||
symbol: "rect",
|
||||
symbolClip: true,
|
||||
symbolSize: [20, 2],
|
||||
symbolPosition: "start",
|
||||
symbolOffset: [0, -1],
|
||||
// symbolBoundingData: this.total,
|
||||
data: yLabel,
|
||||
width: 25,
|
||||
z: 0,
|
||||
zlevel: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
const chart = ref<any>("");
|
||||
|
||||
onMounted(() => {
|
||||
// 图表初始化
|
||||
chart.value = markRaw(
|
||||
echarts.init(document.getElementById(props.id) as HTMLDivElement)
|
||||
);
|
||||
|
||||
chart.value.setOption(options);
|
||||
|
||||
// 大小自适应
|
||||
window.addEventListener("resize", () => {
|
||||
chart.value.resize();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :id="id" class="line-chart"></div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.line-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
108
src/components/Charts/Line.vue
Normal file
@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: "lineChart",
|
||||
},
|
||||
});
|
||||
|
||||
const colors = "RGBA(30, 214, 255,";
|
||||
|
||||
const options = {
|
||||
color: [colors + "1)"],
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "#4B4F52",
|
||||
borderColor: "#4B4F52",
|
||||
padding: 8,
|
||||
textStyle: {
|
||||
color: "#fff",
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: "3%",
|
||||
right: "4%",
|
||||
bottom: "10%",
|
||||
top: "10%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
boundaryGap: false,
|
||||
axisLabel: {
|
||||
textStyle: {
|
||||
color: "#ffffff", //更改坐标轴文字颜色
|
||||
fontSize: 12, //更改坐标轴文字大小
|
||||
// fontFamily: "SourceHanSansCN",
|
||||
},
|
||||
},
|
||||
data: ["00:00", "01:00", "02:00", "03:00"],
|
||||
},
|
||||
yAxis: {
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "rgba(255, 255, 255, .2)",
|
||||
type: "dashed",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
textStyle: {
|
||||
color: "#ffffff", //更改坐标轴文字颜色
|
||||
fontSize: 12, //更改坐标轴文字大小
|
||||
// fontFamily: "SourceHanSansCN",
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "",
|
||||
type: "line",
|
||||
showSymbol: false,
|
||||
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: colors + "0.6)",
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: colors + "0)",
|
||||
},
|
||||
]),
|
||||
},
|
||||
|
||||
data: [30, 50, 60, 71],
|
||||
},
|
||||
],
|
||||
};
|
||||
const chart = ref<any>("");
|
||||
|
||||
onMounted(() => {
|
||||
// 图表初始化
|
||||
chart.value = markRaw(
|
||||
echarts.init(document.getElementById(props.id) as HTMLDivElement)
|
||||
);
|
||||
|
||||
chart.value.setOption(options);
|
||||
|
||||
// 大小自适应
|
||||
window.addEventListener("resize", () => {
|
||||
chart.value.resize();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :id="id" class="line-chart"></div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.line-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
236
src/components/Charts/Ring.vue
Normal file
@ -0,0 +1,236 @@
|
||||
<script setup lang="ts">
|
||||
import * as echarts from "echarts";
|
||||
|
||||
interface ChartDataType {
|
||||
name: string;
|
||||
value: number;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const chartObj = reactive({
|
||||
online: 0,
|
||||
offline: 0,
|
||||
rate: 0,
|
||||
onlineIcon: "",
|
||||
offlineIcon: "",
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: "barChart",
|
||||
},
|
||||
|
||||
chartData: {
|
||||
type: Array as () => ChartDataType[],
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.chartData,
|
||||
(newData) => {
|
||||
if (newData) {
|
||||
const total = newData[0].value + newData[1].value;
|
||||
chartObj.online = newData[0].value;
|
||||
chartObj.offline = newData[1].value;
|
||||
chartObj.rate = Math.round((newData[0].value / total) * 100);
|
||||
chartObj.onlineIcon = newData[0].icon;
|
||||
chartObj.offlineIcon = newData[1].icon;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
const isColor = ["rgba(79, 203, 255, 1)", "rgba(140, 225, 251,1)"];
|
||||
|
||||
const options = {
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
title: {
|
||||
text: "{a|" + chartObj.rate + "}{c|%}",
|
||||
x: "40.5%",
|
||||
y: "40%",
|
||||
textStyle: {
|
||||
rich: {
|
||||
a: {
|
||||
fontSize: 29, //环内数据字体大小
|
||||
color: "#dbe2ea",
|
||||
fontWeight: "bold",
|
||||
fontFamily: "SourceHanSansCN",
|
||||
},
|
||||
c: {
|
||||
fontSize: 15,
|
||||
color: "#dbe2ea",
|
||||
fontWeight: "500",
|
||||
fontFamily: "SourceHanSansCN",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
legendHoverLink: false, //只需要将这个给 false 即可
|
||||
center: ["50.1%", "50%"],
|
||||
name: "",
|
||||
type: "pie",
|
||||
radius: ["85%", "75%"],
|
||||
silent: true,
|
||||
clockwise: true,
|
||||
startAngle: 90,
|
||||
z: 0,
|
||||
zlevel: 0,
|
||||
label: {
|
||||
normal: {
|
||||
position: "center",
|
||||
},
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: chartObj.online,
|
||||
name: "在线",
|
||||
label: {
|
||||
normal: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: {
|
||||
// 圆环的颜色
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: isColor[0], // 0% 处的颜色
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: isColor[1], // 100% 处的颜色
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
value: chartObj.offline,
|
||||
name: "离线",
|
||||
label: {
|
||||
normal: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: "rgba(86, 118, 139, 1)",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const chart = ref<any>("");
|
||||
|
||||
onMounted(() => {
|
||||
// 图表初始化
|
||||
chart.value = markRaw(
|
||||
echarts.init(document.getElementById(props.id) as HTMLDivElement)
|
||||
);
|
||||
|
||||
chart.value.setOption(options);
|
||||
|
||||
// 大小自适应
|
||||
window.addEventListener("resize", () => {
|
||||
chart.value.resize();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ring">
|
||||
<div class="ring-left">
|
||||
<img :src="chartObj.onlineIcon" alt="" />
|
||||
<p class="data">{{ chartObj.online }}</p>
|
||||
<p class="name">设备在线</p>
|
||||
</div>
|
||||
<div class="ring-content">
|
||||
<div :id="id" class="ring-content-chart"></div>
|
||||
<img src="@/assets/images/ring.png" />
|
||||
</div>
|
||||
<div class="ring-right">
|
||||
<img :src="chartObj.offlineIcon" alt="" />
|
||||
<p class="data">{{ chartObj.offline }}</p>
|
||||
<p class="name">设备离线</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@mixin data-style($color) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100px;
|
||||
height: 120px;
|
||||
margin-top: 30px;
|
||||
|
||||
.data {
|
||||
margin-top: 8px;
|
||||
margin-bottom: -13px;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
color: #21cffe;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 12px;
|
||||
color: #fefffe;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.ring {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 20px;
|
||||
|
||||
&-left {
|
||||
@include data-style(#21cffe);
|
||||
}
|
||||
|
||||
&-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: 194px;
|
||||
height: 151px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
@include data-style(#f6ff01);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
4
src/components/Form/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { default as BasicForm } from "./src/BasicForm2.vue";
|
||||
export { useForm } from "./src/hooks/useForm";
|
||||
export * from "./src/types/form";
|
||||
export * from "./src/types/index";
|
||||
336
src/components/Form/src/BasicForm.vue
Normal file
@ -0,0 +1,336 @@
|
||||
<template>
|
||||
<n-form v-bind="getBindValue" :model="formModel" ref="formElRef">
|
||||
<n-grid v-bind="getGrid">
|
||||
<n-gi
|
||||
v-bind="schema.giProps"
|
||||
v-for="schema in getSchema"
|
||||
:key="schema.field"
|
||||
>
|
||||
<n-form-item :label="schema.label" :path="schema.field">
|
||||
<!--标签名右侧温馨提示-->
|
||||
<template #label v-if="schema.labelMessage">
|
||||
{{ schema.label }}
|
||||
<n-tooltip trigger="hover" :style="schema.labelMessageStyle">
|
||||
<template #trigger>
|
||||
<n-icon size="18" class="text-gray-400 cursor-pointer">
|
||||
<QuestionCircleOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ schema.labelMessage }}
|
||||
</n-tooltip>
|
||||
</template>
|
||||
|
||||
<!--判断插槽-->
|
||||
<template v-if="schema.slot">
|
||||
<slot
|
||||
:name="schema.slot"
|
||||
:model="formModel"
|
||||
:field="schema.field"
|
||||
:value="formModel[schema.field]"
|
||||
></slot>
|
||||
</template>
|
||||
|
||||
<!--NCheckbox-->
|
||||
<template v-else-if="schema.component === 'NCheckbox'">
|
||||
<n-checkbox-group v-model:value="formModel[schema.field]">
|
||||
<n-space>
|
||||
<n-checkbox
|
||||
v-for="item in schema.componentProps.options"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
/>
|
||||
</n-space>
|
||||
</n-checkbox-group>
|
||||
</template>
|
||||
|
||||
<!--NRadioGroup-->
|
||||
<template v-else-if="schema.component === 'NRadioGroup'">
|
||||
<n-radio-group v-model:value="formModel[schema.field]">
|
||||
<n-space>
|
||||
<n-radio
|
||||
v-for="item in schema.componentProps.options"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</n-radio>
|
||||
</n-space>
|
||||
</n-radio-group>
|
||||
</template>
|
||||
<!--动态渲染表单组件-->
|
||||
<component
|
||||
v-else
|
||||
v-bind="getComponentProps(schema)"
|
||||
:is="schema.component"
|
||||
v-model:value="formModel[schema.field]"
|
||||
:class="{ isFull: schema.isFull != false && getProps.isFull }"
|
||||
/>
|
||||
<!--组件后面的内容-->
|
||||
<template v-if="schema.suffix">
|
||||
<slot
|
||||
:name="schema.suffix"
|
||||
:model="formModel"
|
||||
:field="schema.field"
|
||||
:value="formModel[schema.field]"
|
||||
></slot>
|
||||
</template>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<!--提交 重置 展开 收起 按钮-->
|
||||
<n-gi
|
||||
:span="isInline ? '' : 24"
|
||||
:suffix="isInline ? true : false"
|
||||
#="{ overflow }"
|
||||
v-if="getProps.showActionButtonGroup"
|
||||
>
|
||||
<n-space
|
||||
align="center"
|
||||
:justify="isInline ? 'end' : 'start'"
|
||||
:style="{ 'margin-left': `${isInline ? 12 : getProps.labelWidth}px` }"
|
||||
>
|
||||
<n-button
|
||||
v-if="getProps.showSubmitButton"
|
||||
v-bind="getSubmitBtnOptions"
|
||||
@click="handleSubmit"
|
||||
:loading="loadingSub"
|
||||
>{{ getProps.submitButtonText }}</n-button
|
||||
>
|
||||
<n-button
|
||||
v-if="getProps.showResetButton"
|
||||
v-bind="getResetBtnOptions"
|
||||
@click="resetFields"
|
||||
>{{ getProps.resetButtonText }}</n-button
|
||||
>
|
||||
<n-button
|
||||
type="primary"
|
||||
text
|
||||
icon-placement="right"
|
||||
v-if="isInline && getProps.showAdvancedButton && overflow"
|
||||
@click="unfoldToggle"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon size="14" class="unfold-icon" v-if="overflow">
|
||||
<DownOutlined />
|
||||
</n-icon>
|
||||
<n-icon size="14" class="unfold-icon" v-else>
|
||||
<UpOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ overflow ? "展开" : "收起" }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
reactive,
|
||||
ref,
|
||||
computed,
|
||||
unref,
|
||||
onMounted,
|
||||
watch,
|
||||
} from "vue";
|
||||
import { createPlaceholderMessage } from "./helper";
|
||||
import { useFormEvents } from "./hooks/useFormEvents";
|
||||
import { useFormValues } from "./hooks/useFormValues";
|
||||
|
||||
import { basicProps } from "./props";
|
||||
import { DownOutlined, UpOutlined, QuestionCircleOutlined } from "@vicons/antd";
|
||||
|
||||
import type { Ref } from "vue";
|
||||
import type { GridProps } from "naive-ui/lib/grid";
|
||||
import type { FormSchema, FormProps, FormActionType } from "./types/form";
|
||||
|
||||
import { isArray, deepMerge } from "@/utils";
|
||||
|
||||
export default defineComponent({
|
||||
name: "BasicForm",
|
||||
components: { DownOutlined, UpOutlined, QuestionCircleOutlined },
|
||||
props: {
|
||||
...basicProps,
|
||||
},
|
||||
emits: ["reset", "submit", "register"],
|
||||
setup(props, { emit, attrs }) {
|
||||
const defaultFormModel = ref<any>({});
|
||||
const formModel = reactive<any>({});
|
||||
const propsRef = ref<Partial<FormProps>>({});
|
||||
const schemaRef = ref(null);
|
||||
const formElRef = ref(null);
|
||||
const gridCollapsed = ref(true);
|
||||
const loadingSub = ref(false);
|
||||
const isUpdateDefaultRef = ref(false);
|
||||
|
||||
const getSubmitBtnOptions = computed(() => {
|
||||
return Object.assign(
|
||||
{
|
||||
size: props.size,
|
||||
type: "primary",
|
||||
},
|
||||
props.submitButtonOptions
|
||||
);
|
||||
});
|
||||
|
||||
const getResetBtnOptions = computed(() => {
|
||||
return Object.assign(
|
||||
{
|
||||
size: props.size,
|
||||
type: "default",
|
||||
},
|
||||
props.resetButtonOptions
|
||||
);
|
||||
});
|
||||
|
||||
function getComponentProps(schema) {
|
||||
const compProps = schema.componentProps ?? {};
|
||||
const component = schema.component;
|
||||
return {
|
||||
clearable: true,
|
||||
placeholder: createPlaceholderMessage(unref(component)),
|
||||
...compProps,
|
||||
};
|
||||
}
|
||||
|
||||
const getProps = computed((): FormProps => {
|
||||
const formProps = { ...props, ...unref(propsRef) } as FormProps;
|
||||
const rulesObj: any = {
|
||||
rules: {},
|
||||
};
|
||||
const schemas: any = formProps.schemas || [];
|
||||
schemas.forEach((item) => {
|
||||
if (item.rules && isArray(item.rules)) {
|
||||
rulesObj.rules[item.field] = item.rules;
|
||||
}
|
||||
});
|
||||
return { ...formProps, ...unref(rulesObj) };
|
||||
});
|
||||
|
||||
const isInline = computed(() => {
|
||||
const { layout } = unref(getProps);
|
||||
return layout === "inline";
|
||||
});
|
||||
|
||||
const getGrid = computed((): GridProps => {
|
||||
const { gridProps } = unref(getProps);
|
||||
return {
|
||||
...gridProps,
|
||||
collapsed: isInline.value ? gridCollapsed.value : false,
|
||||
responsive: "screen",
|
||||
};
|
||||
});
|
||||
|
||||
const getBindValue = computed(
|
||||
() => ({ ...attrs, ...props, ...unref(getProps) } as any)
|
||||
);
|
||||
|
||||
const getSchema = computed((): FormSchema[] => {
|
||||
const schemas: FormSchema[] =
|
||||
unref(schemaRef) || (unref(getProps).schemas as any);
|
||||
for (const schema of schemas) {
|
||||
const { defaultValue } = schema;
|
||||
// handle date type
|
||||
// dateItemType.includes(component as string)
|
||||
if (defaultValue) {
|
||||
schema.defaultValue = defaultValue;
|
||||
}
|
||||
}
|
||||
return schemas as FormSchema[];
|
||||
});
|
||||
|
||||
const { handleFormValues, initDefault } = useFormValues({
|
||||
defaultFormModel,
|
||||
getSchema,
|
||||
formModel,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
validate,
|
||||
resetFields,
|
||||
getFieldsValue,
|
||||
clearValidate,
|
||||
setFieldsValue,
|
||||
} = useFormEvents({
|
||||
emit,
|
||||
getProps,
|
||||
formModel,
|
||||
getSchema,
|
||||
formElRef: formElRef as Ref<any>,
|
||||
defaultFormModel,
|
||||
loadingSub,
|
||||
handleFormValues,
|
||||
});
|
||||
|
||||
function unfoldToggle() {
|
||||
gridCollapsed.value = !gridCollapsed.value;
|
||||
}
|
||||
|
||||
async function setProps(formProps: Partial<FormProps>): Promise<void> {
|
||||
propsRef.value = deepMerge(unref(propsRef) || {}, formProps);
|
||||
}
|
||||
|
||||
const formActionType: Partial<FormActionType> = {
|
||||
getFieldsValue,
|
||||
setFieldsValue,
|
||||
resetFields,
|
||||
validate,
|
||||
clearValidate,
|
||||
setProps,
|
||||
submit: handleSubmit,
|
||||
};
|
||||
|
||||
watch(
|
||||
() => getSchema.value,
|
||||
(schema) => {
|
||||
if (unref(isUpdateDefaultRef)) {
|
||||
return;
|
||||
}
|
||||
if (schema?.length) {
|
||||
initDefault();
|
||||
isUpdateDefaultRef.value = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
initDefault();
|
||||
emit("register", formActionType);
|
||||
});
|
||||
|
||||
return {
|
||||
formElRef,
|
||||
formModel,
|
||||
getGrid,
|
||||
getProps,
|
||||
getBindValue,
|
||||
getSchema,
|
||||
getSubmitBtnOptions,
|
||||
getResetBtnOptions,
|
||||
handleSubmit,
|
||||
resetFields,
|
||||
loadingSub,
|
||||
isInline,
|
||||
getComponentProps,
|
||||
unfoldToggle,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.isFull {
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.unfold-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
margin-left: -3px;
|
||||
}
|
||||
</style>
|
||||
302
src/components/Form/src/BasicForm2.vue
Normal file
@ -0,0 +1,302 @@
|
||||
<script setup lang="ts">
|
||||
import { createPlaceholderMessage } from "./helper";
|
||||
import { useFormEvents } from "./hooks/useFormEvents";
|
||||
import { useFormValues } from "./hooks/useFormValues";
|
||||
import { basicProps } from "./props";
|
||||
import { DownOutlined, UpOutlined, QuestionCircleOutlined } from "@vicons/antd";
|
||||
import { isArray, deepMerge } from "@/utils";
|
||||
import type { FormSchema, FormProps, FormActionType } from "./types/form";
|
||||
const props = defineProps(basicProps);
|
||||
const emit = defineEmits(["reset", "submit", "register"]);
|
||||
|
||||
const defaultFormModel = ref({});
|
||||
const formModel = reactive({});
|
||||
const propsRef = ref({});
|
||||
const schemaRef = ref(null);
|
||||
const formElRef = ref(null);
|
||||
const gridCollapsed = ref(true);
|
||||
const loadingSub = ref(false);
|
||||
const isUpdateDefaultRef = ref(false);
|
||||
|
||||
const getSubmitBtnOptions = computed(() => {
|
||||
return Object.assign(
|
||||
{
|
||||
size: props.size,
|
||||
type: "primary",
|
||||
},
|
||||
props.submitButtonOptions
|
||||
);
|
||||
});
|
||||
|
||||
const getResetBtnOptions = computed(() => {
|
||||
return Object.assign(
|
||||
{
|
||||
size: props.size,
|
||||
type: "default",
|
||||
},
|
||||
props.resetButtonOptions
|
||||
);
|
||||
});
|
||||
|
||||
function getComponentProps(schema) {
|
||||
const compProps = schema.componentProps ?? {};
|
||||
const component = schema.component;
|
||||
return {
|
||||
clearable: true,
|
||||
placeholder: createPlaceholderMessage(unref(component)),
|
||||
...compProps,
|
||||
};
|
||||
}
|
||||
|
||||
const getProps = computed(() => {
|
||||
const formProps = { ...props, ...unref(propsRef) } as FormProps;
|
||||
const rulesObj: any = {
|
||||
rules: {},
|
||||
};
|
||||
const schemas: any = formProps.schemas || [];
|
||||
schemas.forEach((item) => {
|
||||
if (item.rules && isArray(item.rules)) {
|
||||
rulesObj.rules[item.field] = item.rules;
|
||||
}
|
||||
});
|
||||
return { ...formProps, ...unref(rulesObj) };
|
||||
});
|
||||
|
||||
const isInline = computed(() => {
|
||||
const { layout } = unref(getProps);
|
||||
return layout === "inline";
|
||||
});
|
||||
|
||||
const getGrid = computed(() => {
|
||||
const { gridProps } = unref(getProps);
|
||||
return {
|
||||
...gridProps,
|
||||
collapsed: isInline.value ? gridCollapsed.value : false,
|
||||
responsive: "screen",
|
||||
};
|
||||
});
|
||||
|
||||
const getBindValue = computed(() => {
|
||||
return { ...props, ...unref(getProps) };
|
||||
});
|
||||
|
||||
const getSchema = computed(() => {
|
||||
const schemas: FormSchema[] =
|
||||
unref(schemaRef) || (unref(getProps).schemas as any);
|
||||
for (const schema of schemas) {
|
||||
const { defaultValue } = schema;
|
||||
if (defaultValue) {
|
||||
schema.defaultValue = defaultValue;
|
||||
}
|
||||
}
|
||||
return schemas as FormSchema[];
|
||||
});
|
||||
|
||||
const { handleFormValues, initDefault } = useFormValues({
|
||||
defaultFormModel,
|
||||
getSchema,
|
||||
formModel,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
validate,
|
||||
resetFields,
|
||||
getFieldsValue,
|
||||
clearValidate,
|
||||
setFieldsValue,
|
||||
} = useFormEvents({
|
||||
emit: emit as any,
|
||||
getProps,
|
||||
formModel,
|
||||
getSchema,
|
||||
formElRef: formElRef as Ref<any>,
|
||||
defaultFormModel,
|
||||
loadingSub,
|
||||
handleFormValues,
|
||||
});
|
||||
|
||||
function unfoldToggle() {
|
||||
gridCollapsed.value = !gridCollapsed.value;
|
||||
}
|
||||
|
||||
async function setProps(formProps: Partial<FormProps>): Promise<void> {
|
||||
propsRef.value = deepMerge(unref(propsRef) || {}, formProps);
|
||||
}
|
||||
|
||||
const formActionType: Partial<FormActionType> = {
|
||||
getFieldsValue,
|
||||
setFieldsValue,
|
||||
resetFields,
|
||||
validate,
|
||||
clearValidate,
|
||||
setProps,
|
||||
submit: handleSubmit,
|
||||
};
|
||||
|
||||
watch(
|
||||
() => getSchema.value,
|
||||
(schema) => {
|
||||
if (unref(isUpdateDefaultRef)) {
|
||||
return;
|
||||
}
|
||||
if (schema?.length) {
|
||||
initDefault();
|
||||
isUpdateDefaultRef.value = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
initDefault();
|
||||
emit("register", formActionType);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-form v-bind="getBindValue" :model="formModel" ref="formElRef">
|
||||
<n-grid v-bind="getGrid">
|
||||
<n-gi
|
||||
v-bind="schema.giProps"
|
||||
v-for="schema in getSchema"
|
||||
:key="schema.field"
|
||||
>
|
||||
<n-form-item :label="schema.label" :path="schema.field">
|
||||
<!-- 标签名右侧温馨提示 -->
|
||||
<template #label v-if="schema.labelMessage">
|
||||
{{ schema.label }}
|
||||
<n-tooltip trigger="hover" :style="schema.labelMessageStyle">
|
||||
<template #trigger>
|
||||
<n-icon size="18" class="text-gray-400 cursor-pointer">
|
||||
<QuestionCircleOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ schema.labelMessage }}
|
||||
</n-tooltip>
|
||||
</template>
|
||||
|
||||
<!-- 判断插槽 -->
|
||||
<template v-if="schema.slot">
|
||||
<slot
|
||||
:name="schema.slot"
|
||||
:model="formModel"
|
||||
:field="schema.field"
|
||||
:value="formModel[schema.field]"
|
||||
></slot>
|
||||
</template>
|
||||
|
||||
<!-- NCheckbox -->
|
||||
<template v-else-if="schema.component === 'NCheckbox'">
|
||||
<n-checkbox-group v-model:value="formModel[schema.field]">
|
||||
<n-space>
|
||||
<n-checkbox
|
||||
v-for="item in schema.componentProps?.options || []"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
/>
|
||||
</n-space>
|
||||
</n-checkbox-group>
|
||||
</template>
|
||||
|
||||
<!-- NRadioGroup -->
|
||||
<template v-else-if="schema.component === 'NRadioGroup'">
|
||||
<n-radio-group v-model:value="formModel[schema.field]">
|
||||
<n-space>
|
||||
<n-radio
|
||||
v-for="item in schema.componentProps?.options || []"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</n-radio>
|
||||
</n-space>
|
||||
</n-radio-group>
|
||||
</template>
|
||||
|
||||
<!-- 动态渲染表单组件 -->
|
||||
<component
|
||||
v-else
|
||||
v-bind="getComponentProps(schema)"
|
||||
:is="schema.component"
|
||||
v-model:value="formModel[schema.field]"
|
||||
:class="{ isFull: schema.isFull != false && getProps.isFull }"
|
||||
/>
|
||||
|
||||
<!-- 组件后面的内容 -->
|
||||
<template v-if="schema.suffix">
|
||||
<slot
|
||||
:name="schema.suffix"
|
||||
:model="formModel"
|
||||
:field="schema.field"
|
||||
:value="formModel[schema.field]"
|
||||
></slot>
|
||||
</template>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
|
||||
<!-- 提交 重置 展开 收起 按钮 -->
|
||||
<n-gi
|
||||
:span="isInline ? '' : 24"
|
||||
:suffix="isInline ? true : false"
|
||||
#="{ overflow }"
|
||||
v-if="getProps.showActionButtonGroup"
|
||||
>
|
||||
<n-space
|
||||
align="center"
|
||||
:justify="isInline ? 'end' : 'start'"
|
||||
:style="{ 'margin-left': `${isInline ? 12 : getProps.labelWidth}px` }"
|
||||
>
|
||||
<n-button
|
||||
v-if="getProps.showSubmitButton"
|
||||
v-bind="getSubmitBtnOptions"
|
||||
@click="handleSubmit"
|
||||
:loading="loadingSub"
|
||||
>
|
||||
{{ getProps.submitButtonText }}
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
v-if="getProps.showResetButton"
|
||||
v-bind="getResetBtnOptions"
|
||||
@click="resetFields"
|
||||
>
|
||||
{{ getProps.resetButtonText }}
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
type="primary"
|
||||
text
|
||||
icon-placement="right"
|
||||
v-if="isInline && getProps.showAdvancedButton"
|
||||
@click="unfoldToggle"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon size="14" class="unfold-icon" v-if="overflow">
|
||||
<DownOutlined />
|
||||
</n-icon>
|
||||
<n-icon size="14" class="unfold-icon" v-else>
|
||||
<UpOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ overflow ? "展开" : "收起" }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.isFull {
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.unfold-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
margin-left: -3px;
|
||||
}
|
||||
</style>
|
||||
48
src/components/Form/src/helper.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { ComponentType } from "./types/index";
|
||||
|
||||
/**
|
||||
* @description: 生成placeholder
|
||||
*/
|
||||
export function createPlaceholderMessage(component: ComponentType) {
|
||||
if (component === "NInput") return "请输入";
|
||||
if (
|
||||
[
|
||||
"NPicker",
|
||||
"NSelect",
|
||||
"NCheckbox",
|
||||
"NRadio",
|
||||
"NSwitch",
|
||||
"NDatePicker",
|
||||
"NTimePicker",
|
||||
].includes(component)
|
||||
)
|
||||
return "请选择";
|
||||
return "";
|
||||
}
|
||||
|
||||
const DATE_TYPE = ["DatePicker", "MonthPicker", "WeekPicker", "TimePicker"];
|
||||
|
||||
function genType() {
|
||||
return [...DATE_TYPE, "RangePicker"];
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间字段
|
||||
*/
|
||||
export const dateItemType = genType();
|
||||
|
||||
export function defaultType(component) {
|
||||
if (component === "NInput") return "";
|
||||
if (component === "NInputNumber") return null;
|
||||
return [
|
||||
"NPicker",
|
||||
"NSelect",
|
||||
"NCheckbox",
|
||||
"NRadio",
|
||||
"NSwitch",
|
||||
"NDatePicker",
|
||||
"NTimePicker",
|
||||
].includes(component)
|
||||
? ""
|
||||
: undefined;
|
||||
}
|
||||
102
src/components/Form/src/hooks/useForm.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import type {
|
||||
FormProps,
|
||||
FormActionType,
|
||||
UseFormReturnType,
|
||||
} from "../types/form";
|
||||
import type { ComputedRef, Ref } from "vue";
|
||||
import { ref, onUnmounted, unref, nextTick, watch } from "vue";
|
||||
import { isProdMode } from "@/utils/env";
|
||||
import { getDynamicProps } from "@/utils";
|
||||
|
||||
type DynamicProps<T> = {
|
||||
[P in keyof T]: Ref<T[P]> | T[P] | ComputedRef<T[P]>;
|
||||
};
|
||||
|
||||
type Props = Partial<DynamicProps<FormProps>>;
|
||||
|
||||
export function useForm(props?: Props): UseFormReturnType {
|
||||
const formRef = ref<Nullable<FormActionType>>(null);
|
||||
const loadedRef = ref<Nullable<boolean>>(false);
|
||||
|
||||
async function getForm() {
|
||||
const form = unref(formRef);
|
||||
if (!form) {
|
||||
console.error(
|
||||
"The form instance has not been obtained, please make sure that the form has been rendered when performing the form operation!"
|
||||
);
|
||||
}
|
||||
await nextTick();
|
||||
return form as FormActionType;
|
||||
}
|
||||
|
||||
function register(instance: FormActionType) {
|
||||
isProdMode() &&
|
||||
onUnmounted(() => {
|
||||
formRef.value = null;
|
||||
loadedRef.value = null;
|
||||
});
|
||||
if (unref(loadedRef) && isProdMode() && instance === unref(formRef)) return;
|
||||
|
||||
formRef.value = instance;
|
||||
loadedRef.value = true;
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
() => {
|
||||
props && instance.setProps(getDynamicProps(props));
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const methods: FormActionType = {
|
||||
setProps: async (formProps: Partial<FormProps>) => {
|
||||
const form = await getForm();
|
||||
await form.setProps(formProps);
|
||||
},
|
||||
|
||||
resetFields: async () => {
|
||||
getForm().then(async (form) => {
|
||||
await form.resetFields();
|
||||
});
|
||||
},
|
||||
|
||||
clearValidate: async (name?: string | string[]) => {
|
||||
const form = await getForm();
|
||||
await form.clearValidate(name);
|
||||
},
|
||||
|
||||
getFieldsValue: <T>() => {
|
||||
return unref(formRef)?.getFieldsValue() as T;
|
||||
},
|
||||
|
||||
setFieldsValue: async <T>(values: T) => {
|
||||
const form = await getForm();
|
||||
await form.setFieldsValue<T>(values);
|
||||
},
|
||||
|
||||
submit: async (): Promise<any> => {
|
||||
const form = await getForm();
|
||||
return form.submit();
|
||||
},
|
||||
|
||||
validate: async (nameList?: any[]): Promise<any> => {
|
||||
const form = await getForm();
|
||||
return form.validate(nameList);
|
||||
},
|
||||
|
||||
setLoading: (value: boolean) => {
|
||||
loadedRef.value = value;
|
||||
},
|
||||
|
||||
setSchema: async (values) => {
|
||||
const form = await getForm();
|
||||
form.setSchema(values);
|
||||
},
|
||||
};
|
||||
|
||||
return [register, methods];
|
||||
}
|
||||
11
src/components/Form/src/hooks/useFormContext.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { provide, inject } from "vue";
|
||||
|
||||
const key = Symbol("formElRef");
|
||||
|
||||
export function createFormContext(instance) {
|
||||
provide(key, instance);
|
||||
}
|
||||
|
||||
export function useFormContext() {
|
||||
return inject(key);
|
||||
}
|
||||
116
src/components/Form/src/hooks/useFormEvents.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import type { ComputedRef, Ref } from "vue";
|
||||
import type { FormProps, FormSchema, FormActionType } from "../types/form";
|
||||
import { unref, toRaw } from "vue";
|
||||
import { isFunction } from "@/utils";
|
||||
|
||||
declare type EmitType = (event: string, ...args: any[]) => void;
|
||||
|
||||
interface UseFormActionContext {
|
||||
emit: EmitType;
|
||||
getProps: ComputedRef<FormProps>;
|
||||
getSchema: ComputedRef<FormSchema[]>;
|
||||
formModel: any;
|
||||
formElRef: Ref<FormActionType>;
|
||||
defaultFormModel: any;
|
||||
loadingSub: Ref<boolean>;
|
||||
handleFormValues: Function;
|
||||
}
|
||||
|
||||
export function useFormEvents({
|
||||
emit,
|
||||
getProps,
|
||||
formModel,
|
||||
getSchema,
|
||||
formElRef,
|
||||
defaultFormModel,
|
||||
loadingSub,
|
||||
handleFormValues,
|
||||
}: UseFormActionContext) {
|
||||
// 验证
|
||||
async function validate() {
|
||||
return unref(formElRef)?.validate();
|
||||
}
|
||||
|
||||
// 提交
|
||||
async function handleSubmit(e?: Event): Promise<object | boolean> {
|
||||
e && e.preventDefault();
|
||||
loadingSub.value = true;
|
||||
const { submitFunc } = unref(getProps);
|
||||
if (submitFunc && isFunction(submitFunc)) {
|
||||
await submitFunc();
|
||||
loadingSub.value = false;
|
||||
return false;
|
||||
}
|
||||
const formEl = unref(formElRef);
|
||||
if (!formEl) return false;
|
||||
try {
|
||||
await validate();
|
||||
const values = getFieldsValue();
|
||||
loadingSub.value = false;
|
||||
emit("submit", values);
|
||||
return values;
|
||||
} catch (error: any) {
|
||||
emit("submit", false);
|
||||
loadingSub.value = false;
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//清空校验
|
||||
async function clearValidate() {
|
||||
// @ts-ignore
|
||||
await unref(formElRef)?.restoreValidation();
|
||||
}
|
||||
|
||||
//重置
|
||||
async function resetFields(): Promise<void> {
|
||||
const { resetFunc, submitOnReset } = unref(getProps);
|
||||
resetFunc && isFunction(resetFunc) && (await resetFunc());
|
||||
|
||||
const formEl = unref(formElRef);
|
||||
if (!formEl) return;
|
||||
Object.keys(formModel).forEach((key) => {
|
||||
formModel[key] = unref(defaultFormModel)[key] || null;
|
||||
});
|
||||
await clearValidate();
|
||||
const fromValues = handleFormValues(toRaw(unref(formModel)));
|
||||
emit("reset", fromValues);
|
||||
submitOnReset && (await handleSubmit());
|
||||
}
|
||||
|
||||
//获取表单值
|
||||
function getFieldsValue(): any {
|
||||
const formEl = unref(formElRef);
|
||||
if (!formEl) return {};
|
||||
return handleFormValues(toRaw(unref(formModel)));
|
||||
}
|
||||
|
||||
//设置表单字段值
|
||||
async function setFieldsValue(values: any): Promise<void> {
|
||||
const fields = unref(getSchema)
|
||||
.map((item) => item.field)
|
||||
.filter(Boolean);
|
||||
|
||||
Object.keys(values).forEach((key) => {
|
||||
const value = values[key];
|
||||
if (fields.includes(key)) {
|
||||
formModel[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setLoading(value: boolean): void {
|
||||
loadingSub.value = value;
|
||||
}
|
||||
|
||||
return {
|
||||
handleSubmit,
|
||||
validate,
|
||||
resetFields,
|
||||
getFieldsValue,
|
||||
clearValidate,
|
||||
setFieldsValue,
|
||||
setLoading,
|
||||
};
|
||||
}
|
||||
64
src/components/Form/src/hooks/useFormValues.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import {
|
||||
isArray,
|
||||
isFunction,
|
||||
isObject,
|
||||
isString,
|
||||
isNullOrUnDef,
|
||||
} from "@/utils/is";
|
||||
import { unref } from "vue";
|
||||
import type { Ref, ComputedRef } from "vue";
|
||||
import type { FormSchema } from "../types/form";
|
||||
import { set } from "lodash-es";
|
||||
|
||||
interface UseFormValuesContext {
|
||||
defaultFormModel: Ref<any>;
|
||||
getSchema: ComputedRef<FormSchema[]>;
|
||||
formModel: any;
|
||||
}
|
||||
export function useFormValues({
|
||||
defaultFormModel,
|
||||
getSchema,
|
||||
formModel,
|
||||
}: UseFormValuesContext) {
|
||||
// 加工 form values
|
||||
function handleFormValues(values: any) {
|
||||
if (!isObject(values)) {
|
||||
return {};
|
||||
}
|
||||
const res: any = {};
|
||||
for (const item of Object.entries(values)) {
|
||||
let [, value] = item;
|
||||
const [key] = item;
|
||||
if (
|
||||
!key ||
|
||||
(isArray(value) && value.length === 0) ||
|
||||
isFunction(value) ||
|
||||
isNullOrUnDef(value)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// 删除空格
|
||||
if (isString(value)) {
|
||||
value = value.trim();
|
||||
}
|
||||
set(res, key, value);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
//初始化默认值
|
||||
function initDefault() {
|
||||
const schemas = unref(getSchema);
|
||||
const obj: any = {};
|
||||
schemas.forEach((item) => {
|
||||
const { defaultValue } = item;
|
||||
if (!isNullOrUnDef(defaultValue)) {
|
||||
obj[item.field] = defaultValue;
|
||||
formModel[item.field] = defaultValue;
|
||||
}
|
||||
});
|
||||
defaultFormModel.value = obj;
|
||||
}
|
||||
|
||||
return { handleFormValues, initDefault };
|
||||
}
|
||||
82
src/components/Form/src/props.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import type { CSSProperties, PropType } from "vue";
|
||||
import { FormSchema } from "./types/form";
|
||||
import type { GridProps, GridItemProps } from "naive-ui/lib/grid";
|
||||
import type { ButtonProps } from "naive-ui/lib/button";
|
||||
import { propTypes } from "@/utils/propTypes";
|
||||
export const basicProps = {
|
||||
// 标签宽度 固定宽度
|
||||
labelWidth: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
default: 80,
|
||||
},
|
||||
// 表单配置规则
|
||||
schemas: {
|
||||
type: [Array] as PropType<FormSchema[]>,
|
||||
default: () => [],
|
||||
},
|
||||
//布局方式
|
||||
layout: {
|
||||
type: String,
|
||||
default: "inline",
|
||||
},
|
||||
//是否展示为行内表单
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
//大小
|
||||
size: {
|
||||
type: String,
|
||||
default: "medium",
|
||||
},
|
||||
//标签位置
|
||||
labelPlacement: {
|
||||
type: String,
|
||||
default: "left",
|
||||
},
|
||||
//组件是否width 100%
|
||||
isFull: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
//是否显示操作按钮(查询/重置)
|
||||
showActionButtonGroup: propTypes.bool.def(true),
|
||||
// 显示重置按钮
|
||||
showResetButton: propTypes.bool.def(true),
|
||||
//重置按钮配置
|
||||
resetButtonOptions: Object as PropType<Partial<ButtonProps>>,
|
||||
// 显示确认按钮
|
||||
showSubmitButton: propTypes.bool.def(true),
|
||||
// 确认按钮配置
|
||||
submitButtonOptions: Object as PropType<Partial<ButtonProps>>,
|
||||
//展开收起按钮
|
||||
showAdvancedButton: propTypes.bool.def(true),
|
||||
// 确认按钮文字
|
||||
submitButtonText: {
|
||||
type: String,
|
||||
default: "查询",
|
||||
},
|
||||
//重置按钮文字
|
||||
resetButtonText: {
|
||||
type: String,
|
||||
default: "重置",
|
||||
},
|
||||
//grid 配置
|
||||
gridProps: Object as PropType<GridProps>,
|
||||
//gi配置
|
||||
giProps: Object as PropType<GridItemProps>,
|
||||
//grid 样式
|
||||
baseGridStyle: {
|
||||
type: Object as PropType<CSSProperties>,
|
||||
},
|
||||
//是否折叠
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
//默认展示的行数
|
||||
collapsedRows: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
};
|
||||
61
src/components/Form/src/types/form.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { ComponentType } from "./index";
|
||||
import type { CSSProperties } from "vue";
|
||||
import type { GridProps, GridItemProps } from "naive-ui/lib/grid";
|
||||
import type { ButtonProps } from "naive-ui/lib/button";
|
||||
|
||||
export interface FormSchema {
|
||||
field: string;
|
||||
label: string;
|
||||
labelMessage?: string;
|
||||
labelMessageStyle?: object | string;
|
||||
defaultValue?: any;
|
||||
component?: ComponentType;
|
||||
componentProps?: object;
|
||||
slot?: string;
|
||||
rules?: object | object[];
|
||||
giProps?: GridItemProps;
|
||||
isFull?: boolean;
|
||||
suffix?: string;
|
||||
}
|
||||
|
||||
export interface FormProps {
|
||||
model?: any;
|
||||
labelWidth?: number | string;
|
||||
schemas?: FormSchema[];
|
||||
inline: boolean;
|
||||
layout?: string;
|
||||
size: string;
|
||||
labelPlacement: string;
|
||||
isFull: boolean;
|
||||
showActionButtonGroup?: boolean;
|
||||
showResetButton?: boolean;
|
||||
resetButtonOptions?: Partial<ButtonProps>;
|
||||
showSubmitButton?: boolean;
|
||||
showAdvancedButton?: boolean;
|
||||
submitButtonOptions?: Partial<ButtonProps>;
|
||||
submitButtonText?: string;
|
||||
resetButtonText?: string;
|
||||
gridProps?: GridProps;
|
||||
giProps?: GridItemProps;
|
||||
resetFunc?: () => Promise<void>;
|
||||
submitFunc?: () => Promise<void>;
|
||||
submitOnReset?: boolean;
|
||||
baseGridStyle?: CSSProperties;
|
||||
collapsedRows?: number;
|
||||
}
|
||||
|
||||
export interface FormActionType {
|
||||
submit: () => Promise<any>;
|
||||
setProps: (formProps: Partial<FormProps>) => Promise<void>;
|
||||
setSchema: (schemaProps: Partial<FormSchema[]>) => Promise<void>;
|
||||
setFieldsValue: (values: any) => void;
|
||||
clearValidate: (name?: string | string[]) => Promise<void>;
|
||||
getFieldsValue: () => any;
|
||||
resetFields: () => Promise<void>;
|
||||
validate: (nameList?: any[]) => Promise<any>;
|
||||
setLoading: (status: boolean) => void;
|
||||
}
|
||||
|
||||
export type RegisterFn = (formInstance: FormActionType) => void;
|
||||
|
||||
export type UseFormReturnType = [RegisterFn, FormActionType];
|
||||
28
src/components/Form/src/types/index.ts
Normal file
@ -0,0 +1,28 @@
|
||||
export type ComponentType =
|
||||
| "NInput"
|
||||
| "NInputGroup"
|
||||
| "NInputPassword"
|
||||
| "NInputSearch"
|
||||
| "NInputTextArea"
|
||||
| "NInputNumber"
|
||||
| "NInputCountDown"
|
||||
| "NSelect"
|
||||
| "NTreeSelect"
|
||||
| "NRadioButtonGroup"
|
||||
| "NRadioGroup"
|
||||
| "NCheckbox"
|
||||
| "NCheckboxGroup"
|
||||
| "NAutoComplete"
|
||||
| "NCascader"
|
||||
| "NDatePicker"
|
||||
| "NMonthPicker"
|
||||
| "NRangePicker"
|
||||
| "NWeekPicker"
|
||||
| "NTimePicker"
|
||||
| "NSwitch"
|
||||
| "NStrengthMeter"
|
||||
| "NUpload"
|
||||
| "NIconPicker"
|
||||
| "NRender"
|
||||
| "NSlider"
|
||||
| "NRate";
|
||||
45
src/components/SvgIcon/index.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-icon"
|
||||
:style="'width:' + size + ';height:' + size"
|
||||
>
|
||||
<use :xlink:href="symbolId" :fill="color" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
prefix: {
|
||||
type: String,
|
||||
default: "icon",
|
||||
},
|
||||
iconClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "1em",
|
||||
},
|
||||
});
|
||||
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
overflow: hidden;
|
||||
vertical-align: -0.15em; /* 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 */
|
||||
outline: none;
|
||||
fill: currentcolor; /* 定义元素的颜色,currentColor是一个变量,这个变量的值就表示当前元素的color值,如果当前元素未设置color值,则从父元素继承 */
|
||||
}
|
||||
</style>
|
||||
4
src/components/Table/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { default as BasicTable } from "./src/Table.vue";
|
||||
export { default as TableAction } from "./src/components/TableAction.vue";
|
||||
export * from "./src/types/table";
|
||||
export * from "./src/types/tableAction";
|
||||
366
src/components/Table/src/Table.vue
Normal file
@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div class="table-toolbar">
|
||||
<!--顶部左侧区域-->
|
||||
<div class="flex items-center table-toolbar-left">
|
||||
<template v-if="title">
|
||||
<div class="table-toolbar-left-title">
|
||||
{{ title }}
|
||||
<n-tooltip trigger="hover" v-if="titleTooltip">
|
||||
<template #trigger>
|
||||
<n-icon size="18" class="ml-1 text-gray-400 cursor-pointer">
|
||||
<QuestionCircleOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ titleTooltip }}
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<slot name="tableTitle"></slot>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center table-toolbar-right">
|
||||
<!--顶部右侧区域-->
|
||||
<slot name="toolbar"></slot>
|
||||
|
||||
<!--斑马纹-->
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="mr-2 table-toolbar-right-icon">
|
||||
<n-switch v-model:value="isStriped" @update:value="setStriped" />
|
||||
</div>
|
||||
</template>
|
||||
<span>表格斑马纹</span>
|
||||
</n-tooltip>
|
||||
<n-divider vertical />
|
||||
|
||||
<!--刷新-->
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="table-toolbar-right-icon" @click="reload">
|
||||
<n-icon size="18">
|
||||
<ReloadOutlined />
|
||||
</n-icon>
|
||||
</div>
|
||||
</template>
|
||||
<span>刷新</span>
|
||||
</n-tooltip>
|
||||
|
||||
<!--密度-->
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="table-toolbar-right-icon">
|
||||
<n-dropdown
|
||||
@select="densitySelect"
|
||||
trigger="click"
|
||||
:options="densityOptions"
|
||||
v-model:value="tableSize"
|
||||
>
|
||||
<n-icon size="18">
|
||||
<ColumnHeightOutlined />
|
||||
</n-icon>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<span>密度</span>
|
||||
</n-tooltip>
|
||||
|
||||
<!--表格设置单独抽离成组件-->
|
||||
<ColumnSetting />
|
||||
</div>
|
||||
</div>
|
||||
<div class="s-table">
|
||||
<n-data-table
|
||||
ref="tableElRef"
|
||||
v-bind="getBindValues"
|
||||
:striped="isStriped"
|
||||
:pagination="pagination"
|
||||
@update:page="updatePage"
|
||||
@update:page-size="updatePageSize"
|
||||
>
|
||||
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
|
||||
<slot :name="item" v-bind="data"></slot>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
ref,
|
||||
defineComponent,
|
||||
reactive,
|
||||
unref,
|
||||
toRaw,
|
||||
computed,
|
||||
toRefs,
|
||||
onMounted,
|
||||
nextTick,
|
||||
} from "vue";
|
||||
import {
|
||||
ReloadOutlined,
|
||||
ColumnHeightOutlined,
|
||||
QuestionCircleOutlined,
|
||||
} from "@vicons/antd";
|
||||
import { createTableContext } from "./hooks/useTableContext";
|
||||
|
||||
import ColumnSetting from "./components/settings/ColumnSetting.vue";
|
||||
|
||||
import { useLoading } from "./hooks/useLoading";
|
||||
import { useColumns } from "./hooks/useColumns";
|
||||
import { useDataSource } from "./hooks/useDataSource";
|
||||
import { usePagination } from "./hooks/usePagination";
|
||||
|
||||
import { basicProps } from "./props";
|
||||
|
||||
import { BasicTableProps } from "./types/table";
|
||||
|
||||
import { getViewportOffset } from "@/utils";
|
||||
import { useWindowSizeFn } from "@/hooks/event/useWindowSizeFn";
|
||||
import { isBoolean } from "@/utils";
|
||||
|
||||
const densityOptions = [
|
||||
{
|
||||
type: "menu",
|
||||
label: "紧凑",
|
||||
key: "small",
|
||||
},
|
||||
{
|
||||
type: "menu",
|
||||
label: "默认",
|
||||
key: "medium",
|
||||
},
|
||||
{
|
||||
type: "menu",
|
||||
label: "宽松",
|
||||
key: "large",
|
||||
},
|
||||
];
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
ReloadOutlined,
|
||||
ColumnHeightOutlined,
|
||||
ColumnSetting,
|
||||
QuestionCircleOutlined,
|
||||
},
|
||||
props: {
|
||||
...basicProps,
|
||||
},
|
||||
emits: [
|
||||
"fetch-success",
|
||||
"fetch-error",
|
||||
"update:checked-row-keys",
|
||||
"edit-end",
|
||||
"edit-cancel",
|
||||
"edit-row-end",
|
||||
"edit-change",
|
||||
],
|
||||
setup(props, { emit }) {
|
||||
const deviceHeight = ref(150);
|
||||
const tableElRef = ref(null);
|
||||
const wrapRef = ref(null);
|
||||
let paginationEl: HTMLElement | null;
|
||||
const isStriped = ref(false);
|
||||
const tableData = ref<any[]>([]);
|
||||
const innerPropsRef = ref<Partial<BasicTableProps>>();
|
||||
|
||||
const getProps = computed(() => {
|
||||
return { ...props, ...unref(innerPropsRef) } as BasicTableProps;
|
||||
});
|
||||
|
||||
const { getLoading, setLoading } = useLoading(getProps);
|
||||
|
||||
const { getPaginationInfo, setPagination } = usePagination(getProps);
|
||||
|
||||
const { getDataSourceRef, getDataSource, getRowKey, reload } =
|
||||
useDataSource(
|
||||
getProps,
|
||||
{
|
||||
getPaginationInfo,
|
||||
setPagination,
|
||||
tableData,
|
||||
setLoading,
|
||||
},
|
||||
emit
|
||||
);
|
||||
|
||||
const {
|
||||
getPageColumns,
|
||||
setColumns,
|
||||
getColumns,
|
||||
getCacheColumns,
|
||||
setCacheColumnsField,
|
||||
} = useColumns(getProps);
|
||||
|
||||
const state = reactive({
|
||||
tableSize: unref(getProps as any).size || "medium",
|
||||
isColumnSetting: false,
|
||||
});
|
||||
|
||||
//页码切换
|
||||
function updatePage(page) {
|
||||
setPagination({ page: page });
|
||||
reload();
|
||||
}
|
||||
|
||||
//分页数量切换
|
||||
function updatePageSize(size) {
|
||||
setPagination({ page: 1, pageSize: size });
|
||||
reload();
|
||||
}
|
||||
|
||||
//密度切换
|
||||
function densitySelect(e) {
|
||||
state.tableSize = e;
|
||||
}
|
||||
|
||||
//选中行
|
||||
function updateCheckedRowKeys(rowKeys) {
|
||||
emit("update:checked-row-keys", rowKeys);
|
||||
}
|
||||
|
||||
//获取表格大小
|
||||
const getTableSize = computed(() => state.tableSize);
|
||||
|
||||
//组装表格信息
|
||||
const getBindValues = computed(() => {
|
||||
const tableData = unref(getDataSourceRef);
|
||||
const maxHeight = tableData.length ? `${unref(deviceHeight)}px` : "auto";
|
||||
return {
|
||||
...unref(getProps),
|
||||
loading: unref(getLoading),
|
||||
columns: toRaw(unref(getPageColumns)),
|
||||
rowKey: unref(getRowKey),
|
||||
data: tableData,
|
||||
size: unref(getTableSize),
|
||||
remote: true,
|
||||
"max-height": maxHeight,
|
||||
};
|
||||
});
|
||||
|
||||
//获取分页信息
|
||||
const pagination = computed(() => toRaw(unref(getPaginationInfo)));
|
||||
|
||||
function setProps(props: Partial<BasicTableProps>) {
|
||||
innerPropsRef.value = { ...unref(innerPropsRef), ...props };
|
||||
}
|
||||
|
||||
const setStriped = (value: boolean) => (isStriped.value = value);
|
||||
|
||||
const tableAction = {
|
||||
reload,
|
||||
setColumns,
|
||||
setLoading,
|
||||
setProps,
|
||||
getColumns,
|
||||
getPageColumns,
|
||||
getCacheColumns,
|
||||
setCacheColumnsField,
|
||||
emit,
|
||||
};
|
||||
|
||||
const getCanResize = computed(() => {
|
||||
const { canResize } = unref(getProps);
|
||||
return canResize;
|
||||
});
|
||||
|
||||
async function computeTableHeight() {
|
||||
const table = unref(tableElRef);
|
||||
if (!table) return;
|
||||
if (!unref(getCanResize)) return;
|
||||
const tableEl: any = table?.$el;
|
||||
const headEl = tableEl.querySelector(".n-data-table-thead ");
|
||||
const { bottomIncludeBody } = getViewportOffset(headEl);
|
||||
const headerH = 64;
|
||||
let paginationH = 2;
|
||||
let marginH = 24;
|
||||
if (!isBoolean(unref(pagination))) {
|
||||
paginationEl = tableEl.querySelector(
|
||||
".n-data-table__pagination"
|
||||
) as HTMLElement;
|
||||
if (paginationEl) {
|
||||
const offsetHeight = paginationEl.offsetHeight;
|
||||
paginationH += offsetHeight || 0;
|
||||
} else {
|
||||
paginationH += 28;
|
||||
}
|
||||
}
|
||||
let height =
|
||||
bottomIncludeBody -
|
||||
(headerH + paginationH + marginH + (props.resizeHeightOffset || 0));
|
||||
const maxHeight = props.maxHeight;
|
||||
height = maxHeight && maxHeight < height ? maxHeight : height;
|
||||
deviceHeight.value = height;
|
||||
}
|
||||
|
||||
useWindowSizeFn(computeTableHeight, 280);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
computeTableHeight();
|
||||
});
|
||||
});
|
||||
|
||||
createTableContext({ ...tableAction, wrapRef, getBindValues });
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
tableElRef,
|
||||
getBindValues,
|
||||
getDataSource,
|
||||
densityOptions,
|
||||
reload,
|
||||
densitySelect,
|
||||
updatePage,
|
||||
updatePageSize,
|
||||
pagination,
|
||||
tableAction,
|
||||
setStriped,
|
||||
isStriped,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 0 16px;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
|
||||
&-icon {
|
||||
margin-left: 12px;
|
||||
font-size: 16px;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
|
||||
:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-toolbar-inner-popover-title {
|
||||
padding: 2px 0;
|
||||
}
|
||||
</style>
|
||||
42
src/components/Table/src/componentMap.ts
Normal file
@ -0,0 +1,42 @@
|
||||
/* eslint-disable @typescript-eslint/no-duplicate-enum-values */
|
||||
import type { Component } from "vue";
|
||||
import {
|
||||
NInput,
|
||||
NSelect,
|
||||
NCheckbox,
|
||||
NInputNumber,
|
||||
NSwitch,
|
||||
NDatePicker,
|
||||
NTimePicker,
|
||||
} from "naive-ui";
|
||||
import type { ComponentType } from "./types/componentType";
|
||||
|
||||
export enum EventEnum {
|
||||
NInput = "on-input",
|
||||
NInputNumber = "on-input",
|
||||
NSelect = "on-update:value",
|
||||
NSwitch = "on-update:value",
|
||||
NCheckbox = "on-update:value",
|
||||
NDatePicker = "on-update:value",
|
||||
NTimePicker = "on-update:value",
|
||||
}
|
||||
|
||||
const componentMap = new Map<ComponentType, Component>();
|
||||
|
||||
componentMap.set("NInput", NInput);
|
||||
componentMap.set("NInputNumber", NInputNumber);
|
||||
componentMap.set("NSelect", NSelect);
|
||||
componentMap.set("NSwitch", NSwitch);
|
||||
componentMap.set("NCheckbox", NCheckbox);
|
||||
componentMap.set("NDatePicker", NDatePicker);
|
||||
componentMap.set("NTimePicker", NTimePicker);
|
||||
|
||||
export function add(compName: ComponentType, component: Component) {
|
||||
componentMap.set(compName, component);
|
||||
}
|
||||
|
||||
export function del(compName: ComponentType) {
|
||||
componentMap.delete(compName);
|
||||
}
|
||||
|
||||
export { componentMap };
|
||||
155
src/components/Table/src/components/TableAction.vue
Normal file
@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="tableAction">
|
||||
<div class="flex items-center justify-center">
|
||||
<template
|
||||
v-for="(action, index) in getActions"
|
||||
:key="`${index}-${action.label}`"
|
||||
>
|
||||
<n-button v-bind="action" class="mx-1">
|
||||
{{ action.label }}
|
||||
<template #icon v-if="action.hasOwnProperty('icon')">
|
||||
<n-icon :component="action.icon" />
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
<n-dropdown
|
||||
v-if="dropDownActions && getDropdownList.length"
|
||||
trigger="hover"
|
||||
:options="getDropdownList"
|
||||
@select="select"
|
||||
>
|
||||
<slot name="more"></slot>
|
||||
<n-button
|
||||
v-bind="getMoreProps"
|
||||
class="mx-1"
|
||||
v-if="!$slots.more"
|
||||
icon-placement="right"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span>更多</span>
|
||||
<n-icon size="14" class="ml-1">
|
||||
<DownOutlined />
|
||||
</n-icon>
|
||||
</div>
|
||||
<!-- <template #icon>-->
|
||||
<!-- -->
|
||||
<!-- </template>-->
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed, toRaw } from "vue";
|
||||
import { ActionItem } from "@/components/Table";
|
||||
// import { usePermission } from "@/hooks/web/usePermission";
|
||||
import { isBoolean, isFunction } from "@/utils";
|
||||
import { DownOutlined } from "@vicons/antd";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TableAction",
|
||||
components: { DownOutlined },
|
||||
props: {
|
||||
actions: {
|
||||
type: Array as PropType<ActionItem[]>,
|
||||
default: null,
|
||||
required: true,
|
||||
},
|
||||
dropDownActions: {
|
||||
type: Array as PropType<ActionItem[]>,
|
||||
default: null,
|
||||
},
|
||||
style: {
|
||||
type: String as PropType<String>,
|
||||
default: "button",
|
||||
},
|
||||
select: {
|
||||
type: Function as PropType<Function>,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const actionType =
|
||||
props.style === "button"
|
||||
? "default"
|
||||
: props.style === "text"
|
||||
? "primary"
|
||||
: "default";
|
||||
const actionText =
|
||||
props.style === "button"
|
||||
? undefined
|
||||
: props.style === "text"
|
||||
? true
|
||||
: undefined;
|
||||
|
||||
const getMoreProps = computed(() => {
|
||||
return {
|
||||
text: actionText,
|
||||
type: actionType,
|
||||
size: "small",
|
||||
};
|
||||
});
|
||||
|
||||
const getDropdownList = computed(() => {
|
||||
return (toRaw(props.dropDownActions) || [])
|
||||
.filter((action) => {
|
||||
return isIfShow(action);
|
||||
})
|
||||
.map((action) => {
|
||||
const { popConfirm } = action;
|
||||
return {
|
||||
size: "small",
|
||||
text: actionText,
|
||||
type: actionType,
|
||||
...action,
|
||||
...popConfirm,
|
||||
onConfirm: popConfirm?.confirm,
|
||||
onCancel: popConfirm?.cancel,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
function isIfShow(action: ActionItem): boolean {
|
||||
const ifShow = action.ifShow;
|
||||
|
||||
let isIfShow = true;
|
||||
|
||||
if (isBoolean(ifShow)) {
|
||||
isIfShow = ifShow;
|
||||
}
|
||||
if (isFunction(ifShow)) {
|
||||
isIfShow = ifShow(action);
|
||||
}
|
||||
return isIfShow;
|
||||
}
|
||||
|
||||
const getActions = computed(() => {
|
||||
return (toRaw(props.actions) || [])
|
||||
.filter((action) => {
|
||||
return isIfShow(action);
|
||||
})
|
||||
.map((action) => {
|
||||
const { popConfirm } = action;
|
||||
//需要展示什么风格,自己修改一下参数
|
||||
return {
|
||||
size: "small",
|
||||
text: actionText,
|
||||
type: actionType,
|
||||
...action,
|
||||
...(popConfirm || {}),
|
||||
onConfirm: popConfirm?.confirm,
|
||||
onCancel: popConfirm?.cancel,
|
||||
enable: !!popConfirm,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
getActions,
|
||||
getDropdownList,
|
||||
getMoreProps,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,52 @@
|
||||
import type { FunctionalComponent, defineComponent } from "vue";
|
||||
import type { ComponentType } from "../../types/componentType";
|
||||
import { componentMap } from "@/components/Table/src/componentMap";
|
||||
|
||||
import { h } from "vue";
|
||||
|
||||
import { NPopover } from "naive-ui";
|
||||
|
||||
export interface ComponentProps {
|
||||
component: ComponentType;
|
||||
rule: boolean;
|
||||
popoverVisible: boolean;
|
||||
ruleMessage: string;
|
||||
}
|
||||
|
||||
export const CellComponent: FunctionalComponent = (
|
||||
{
|
||||
component = "NInput",
|
||||
rule = true,
|
||||
ruleMessage,
|
||||
popoverVisible,
|
||||
}: ComponentProps,
|
||||
{ attrs }
|
||||
) => {
|
||||
const Comp = componentMap.get(component) as typeof defineComponent;
|
||||
|
||||
const DefaultComp = h(Comp, attrs);
|
||||
if (!rule) {
|
||||
return DefaultComp;
|
||||
}
|
||||
return h(
|
||||
NPopover,
|
||||
{ "display-directive": "show", show: !!popoverVisible, manual: "manual" },
|
||||
{
|
||||
trigger: () => DefaultComp,
|
||||
default: () =>
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
style: {
|
||||
color: "red",
|
||||
width: "90px",
|
||||
display: "inline-block",
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => ruleMessage,
|
||||
}
|
||||
),
|
||||
}
|
||||
);
|
||||
};
|
||||
443
src/components/Table/src/components/editable/EditableCell.vue
Normal file
@ -0,0 +1,443 @@
|
||||
<template>
|
||||
<div class="editable-cell">
|
||||
<div v-show="!isEdit" class="editable-cell-content" @click="handleEdit">
|
||||
{{ getValues }}
|
||||
<n-icon class="edit-icon" v-if="!column.editRow">
|
||||
<FormOutlined />
|
||||
</n-icon>
|
||||
</div>
|
||||
<div
|
||||
class="flex editable-cell-content"
|
||||
v-show="isEdit"
|
||||
v-click-outside="onClickOutside"
|
||||
>
|
||||
<div class="editable-cell-content-comp">
|
||||
<CellComponent
|
||||
v-bind="getComponentProps"
|
||||
:component="getComponent"
|
||||
:popoverVisible="getRuleVisible"
|
||||
:ruleMessage="ruleMessage"
|
||||
:rule="getRule"
|
||||
:class="getWrapperClass"
|
||||
ref="elRef"
|
||||
@options-change="handleOptionsChange"
|
||||
@press-enter="handleEnter"
|
||||
/>
|
||||
</div>
|
||||
<div class="editable-cell-action" v-if="!getRowEditable">
|
||||
<n-icon class="mx-2 cursor-pointer">
|
||||
<CheckOutlined @click="handleSubmit" />
|
||||
</n-icon>
|
||||
<n-icon class="mx-2 cursor-pointer">
|
||||
<CloseOutlined @click="handleCancel" />
|
||||
</n-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { PropType } from "vue";
|
||||
import type { BasicColumn } from "../../types/table";
|
||||
import type { EditRecordRow } from "./index";
|
||||
|
||||
import {
|
||||
defineComponent,
|
||||
ref,
|
||||
unref,
|
||||
nextTick,
|
||||
computed,
|
||||
watchEffect,
|
||||
toRaw,
|
||||
} from "vue";
|
||||
import { FormOutlined, CloseOutlined, CheckOutlined } from "@vicons/antd";
|
||||
import { CellComponent } from "./CellComponent";
|
||||
|
||||
import { useTableContext } from "../../hooks/useTableContext";
|
||||
|
||||
import clickOutside from "@/directives/clickOutside";
|
||||
|
||||
import { propTypes } from "@/utils/propTypes";
|
||||
import { isString, isBoolean, isFunction, isNumber, isArray } from "@/utils";
|
||||
import { createPlaceholderMessage } from "./helper";
|
||||
import { set, omit } from "lodash-es";
|
||||
import { EventEnum } from "@/components/Table/src/componentMap";
|
||||
|
||||
import { parseISO, format } from "date-fns";
|
||||
|
||||
export default defineComponent({
|
||||
name: "EditableCell",
|
||||
components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent },
|
||||
directives: {
|
||||
clickOutside,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Boolean, Object] as PropType<
|
||||
string | number | boolean
|
||||
>,
|
||||
default: "",
|
||||
},
|
||||
record: {
|
||||
type: Object as PropType<EditRecordRow>,
|
||||
},
|
||||
column: {
|
||||
type: Object as PropType<BasicColumn>,
|
||||
default: () => ({}),
|
||||
},
|
||||
index: propTypes.number,
|
||||
},
|
||||
setup(props) {
|
||||
const table = useTableContext();
|
||||
const isEdit = ref(false);
|
||||
const elRef = ref();
|
||||
const ruleVisible = ref(false);
|
||||
const ruleMessage = ref("");
|
||||
const optionsRef = ref<LabelValueOptions>([]);
|
||||
const currentValueRef = ref<any>(props.value);
|
||||
const defaultValueRef = ref<any>(props.value);
|
||||
|
||||
// const { prefixCls } = useDesign('editable-cell');
|
||||
|
||||
const getComponent = computed(
|
||||
() => props.column?.editComponent || "NInput"
|
||||
);
|
||||
const getRule = computed(() => props.column?.editRule);
|
||||
|
||||
const getRuleVisible = computed(() => {
|
||||
return unref(ruleMessage) && unref(ruleVisible);
|
||||
});
|
||||
|
||||
const getIsCheckComp = computed(() => {
|
||||
const component = unref(getComponent);
|
||||
return ["NCheckbox", "NRadio"].includes(component);
|
||||
});
|
||||
|
||||
const getComponentProps = computed(() => {
|
||||
const compProps = props.column?.editComponentProps ?? {};
|
||||
const editComponent = props.column?.editComponent ?? null;
|
||||
const component = unref(getComponent);
|
||||
const apiSelectProps: any = {};
|
||||
|
||||
const isCheckValue = unref(getIsCheckComp);
|
||||
|
||||
let valueField = isCheckValue ? "checked" : "value";
|
||||
const val = unref(currentValueRef);
|
||||
|
||||
let value = isCheckValue
|
||||
? isNumber(val) && isBoolean(val)
|
||||
? val
|
||||
: !!val
|
||||
: val;
|
||||
|
||||
//TODO 特殊处理 NDatePicker 可能要根据项目 规范自行调整代码
|
||||
if (component === "NDatePicker") {
|
||||
if (isString(value)) {
|
||||
if (compProps.valueFormat) {
|
||||
valueField = "formatted-value";
|
||||
} else {
|
||||
value = parseISO(value as any).getTime();
|
||||
}
|
||||
} else if (isArray(value)) {
|
||||
if (compProps.valueFormat) {
|
||||
valueField = "formatted-value";
|
||||
} else {
|
||||
value = value.map((item) => parseISO(item).getTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onEvent: any = editComponent ? EventEnum[editComponent] : undefined;
|
||||
|
||||
return {
|
||||
placeholder: createPlaceholderMessage(unref(getComponent)),
|
||||
...apiSelectProps,
|
||||
...omit(compProps, "onChange"),
|
||||
[onEvent]: handleChange,
|
||||
[valueField]: value,
|
||||
};
|
||||
});
|
||||
|
||||
const getValues = computed(() => {
|
||||
const { editComponentProps, editValueMap } = props.column;
|
||||
|
||||
const value = unref(currentValueRef);
|
||||
|
||||
if (editValueMap && isFunction(editValueMap)) {
|
||||
return editValueMap(value);
|
||||
}
|
||||
|
||||
const component = unref(getComponent);
|
||||
if (!component.includes("NSelect")) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const options: LabelValueOptions =
|
||||
editComponentProps?.options ?? (unref(optionsRef) || []);
|
||||
const option = options.find((item) => `${item.value}` === `${value}`);
|
||||
|
||||
return option?.label ?? value;
|
||||
});
|
||||
|
||||
const getWrapperClass = computed(() => {
|
||||
const { align = "center" } = props.column;
|
||||
return `edit-cell-align-${align}`;
|
||||
});
|
||||
|
||||
const getRowEditable = computed(() => {
|
||||
const { editable } = props.record || {};
|
||||
return !!editable;
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
defaultValueRef.value = props.value;
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
const { editable } = props.column;
|
||||
if (isBoolean(editable) || isBoolean(unref(getRowEditable))) {
|
||||
isEdit.value = !!editable || unref(getRowEditable);
|
||||
}
|
||||
});
|
||||
|
||||
function handleEdit() {
|
||||
if (unref(getRowEditable) || unref(props.column?.editRow)) return;
|
||||
ruleMessage.value = "";
|
||||
isEdit.value = true;
|
||||
nextTick(() => {
|
||||
const el = unref(elRef);
|
||||
el?.focus?.();
|
||||
});
|
||||
}
|
||||
|
||||
async function handleChange(e: any) {
|
||||
const component = unref(getComponent);
|
||||
const compProps = props.column?.editComponentProps ?? {};
|
||||
if (!e) {
|
||||
currentValueRef.value = e;
|
||||
} else if (e?.target && Reflect.has(e.target, "value")) {
|
||||
currentValueRef.value = (e as ChangeEvent).target.value;
|
||||
} else if (component === "NCheckbox") {
|
||||
currentValueRef.value = (e as ChangeEvent).target.checked;
|
||||
} else if (isString(e) || isBoolean(e) || isNumber(e)) {
|
||||
currentValueRef.value = e;
|
||||
}
|
||||
|
||||
//TODO 特殊处理 NDatePicker 可能要根据项目 规范自行调整代码
|
||||
if (component === "NDatePicker") {
|
||||
if (isNumber(currentValueRef.value)) {
|
||||
if (compProps.valueFormat) {
|
||||
currentValueRef.value = format(
|
||||
currentValueRef.value,
|
||||
compProps.valueFormat
|
||||
);
|
||||
}
|
||||
} else if (isArray(currentValueRef.value)) {
|
||||
if (compProps.valueFormat) {
|
||||
currentValueRef.value = currentValueRef.value.map((item) => {
|
||||
format(item, compProps.valueFormat);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onChange = props.column?.editComponentProps?.onChange;
|
||||
if (onChange && isFunction(onChange)) onChange(...arguments);
|
||||
|
||||
table.emit?.("edit-change", {
|
||||
column: props.column,
|
||||
value: unref(currentValueRef),
|
||||
record: toRaw(props.record),
|
||||
});
|
||||
await handleSubmiRule();
|
||||
}
|
||||
|
||||
async function handleSubmiRule() {
|
||||
const { column, record } = props;
|
||||
const { editRule } = column;
|
||||
const currentValue = unref(currentValueRef);
|
||||
|
||||
if (editRule) {
|
||||
if (isBoolean(editRule) && !currentValue && !isNumber(currentValue)) {
|
||||
ruleVisible.value = true;
|
||||
const component = unref(getComponent);
|
||||
ruleMessage.value = createPlaceholderMessage(component);
|
||||
return false;
|
||||
}
|
||||
if (isFunction(editRule)) {
|
||||
const res = await editRule(currentValue, record as any);
|
||||
if (!!res) {
|
||||
ruleMessage.value = res;
|
||||
ruleVisible.value = true;
|
||||
return false;
|
||||
} else {
|
||||
ruleMessage.value = "";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ruleMessage.value = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleSubmit(needEmit = true, valid = true) {
|
||||
if (valid) {
|
||||
const isPass = await handleSubmiRule();
|
||||
if (!isPass) return false;
|
||||
}
|
||||
|
||||
const { column, index, record } = props;
|
||||
if (!record) return false;
|
||||
const { key } = column;
|
||||
const value = unref(currentValueRef);
|
||||
if (!key) return;
|
||||
|
||||
const dataKey = key as string;
|
||||
|
||||
set(record, dataKey, value);
|
||||
//const record = await table.updateTableData(index, dataKey, value);
|
||||
needEmit && table.emit?.("edit-end", { record, index, key, value });
|
||||
isEdit.value = false;
|
||||
}
|
||||
|
||||
async function handleEnter() {
|
||||
if (props.column?.editRow) {
|
||||
return;
|
||||
}
|
||||
await handleSubmit();
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
isEdit.value = false;
|
||||
currentValueRef.value = defaultValueRef.value;
|
||||
const { column, index, record } = props;
|
||||
const { key } = column;
|
||||
ruleVisible.value = true;
|
||||
ruleMessage.value = "";
|
||||
table.emit?.("edit-cancel", {
|
||||
record,
|
||||
index,
|
||||
key: key,
|
||||
value: unref(currentValueRef),
|
||||
});
|
||||
}
|
||||
|
||||
function onClickOutside() {
|
||||
if (props.column?.editable || unref(getRowEditable)) {
|
||||
return;
|
||||
}
|
||||
const component = unref(getComponent);
|
||||
|
||||
if (component.includes("NInput")) {
|
||||
handleCancel();
|
||||
}
|
||||
}
|
||||
|
||||
// only ApiSelect
|
||||
function handleOptionsChange(options: LabelValueOptions) {
|
||||
optionsRef.value = options;
|
||||
}
|
||||
|
||||
function initCbs(cbs: "submitCbs" | "validCbs" | "cancelCbs", handle: Fn) {
|
||||
if (props.record) {
|
||||
/* eslint-disable */
|
||||
isArray(props.record[cbs])
|
||||
? props.record[cbs]?.push(handle)
|
||||
: (props.record[cbs] = [handle]);
|
||||
}
|
||||
}
|
||||
|
||||
if (props.record) {
|
||||
initCbs("submitCbs", handleSubmit);
|
||||
initCbs("validCbs", handleSubmiRule);
|
||||
initCbs("cancelCbs", handleCancel);
|
||||
|
||||
if (props.column.key) {
|
||||
if (!props.record.editValueRefs) props.record.editValueRefs = {};
|
||||
props.record.editValueRefs[props.column.key] = currentValueRef;
|
||||
}
|
||||
/* eslint-disable */
|
||||
props.record.onCancelEdit = () => {
|
||||
isArray(props.record?.cancelCbs) &&
|
||||
props.record?.cancelCbs.forEach((fn) => fn());
|
||||
};
|
||||
/* eslint-disable */
|
||||
props.record.onSubmitEdit = async () => {
|
||||
if (isArray(props.record?.submitCbs)) {
|
||||
const validFns = (props.record?.validCbs || []).map((fn) => fn());
|
||||
|
||||
const res = await Promise.all(validFns);
|
||||
|
||||
const pass = res.every((item) => !!item);
|
||||
|
||||
if (!pass) return;
|
||||
const submitFns = props.record?.submitCbs || [];
|
||||
submitFns.forEach((fn) => fn(false, false));
|
||||
table.emit?.("edit-row-end");
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isEdit,
|
||||
handleEdit,
|
||||
currentValueRef,
|
||||
handleSubmit,
|
||||
handleChange,
|
||||
handleCancel,
|
||||
elRef,
|
||||
getComponent,
|
||||
getRule,
|
||||
onClickOutside,
|
||||
ruleMessage,
|
||||
getRuleVisible,
|
||||
getComponentProps,
|
||||
handleOptionsChange,
|
||||
getWrapperClass,
|
||||
getRowEditable,
|
||||
getValues,
|
||||
handleEnter,
|
||||
// getSize,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.editable-cell {
|
||||
&-content {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
white-space: nowrap;
|
||||
|
||||
&-comp {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
//position: absolute;
|
||||
//top: 4px;
|
||||
//right: 0;
|
||||
display: none;
|
||||
width: 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.edit-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
21
src/components/Table/src/components/editable/helper.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { ComponentType } from "../../types/componentType";
|
||||
|
||||
/**
|
||||
* @description: 生成placeholder
|
||||
*/
|
||||
export function createPlaceholderMessage(component: ComponentType) {
|
||||
if (component === "NInput") return "请输入";
|
||||
if (
|
||||
[
|
||||
"NPicker",
|
||||
"NSelect",
|
||||
"NCheckbox",
|
||||
"NRadio",
|
||||
"NSwitch",
|
||||
"NDatePicker",
|
||||
"NTimePicker",
|
||||
].includes(component)
|
||||
)
|
||||
return "请选择";
|
||||
return "";
|
||||
}
|
||||
49
src/components/Table/src/components/editable/index.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import type { BasicColumn } from "@/components/Table/src/types/table";
|
||||
import { h, Ref } from "vue";
|
||||
|
||||
import EditableCell from "./EditableCell.vue";
|
||||
|
||||
export function renderEditCell(column: BasicColumn) {
|
||||
return (record, index) => {
|
||||
const _key = column.key;
|
||||
const value = record[_key];
|
||||
record.onEdit = async (edit: boolean, submit = false) => {
|
||||
if (!submit) {
|
||||
record.editable = edit;
|
||||
}
|
||||
|
||||
if (!edit && submit) {
|
||||
const res = await record.onSubmitEdit?.();
|
||||
if (res) {
|
||||
record.editable = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// cancel
|
||||
if (!edit && !submit) {
|
||||
record.onCancelEdit?.();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return h(EditableCell, {
|
||||
value,
|
||||
record,
|
||||
column,
|
||||
index,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export type EditRecordRow<T = any> = Partial<
|
||||
{
|
||||
onEdit: (editable: boolean, submit?: boolean) => Promise<boolean>;
|
||||
editable: boolean;
|
||||
onCancel: Fn;
|
||||
onSubmit: Fn;
|
||||
submitCbs: Fn[];
|
||||
cancelCbs: Fn[];
|
||||
validCbs: Fn[];
|
||||
editValueRefs: Recordable<Ref>;
|
||||
} & T
|
||||
>;
|
||||
357
src/components/Table/src/components/settings/ColumnSetting.vue
Normal file
@ -0,0 +1,357 @@
|
||||
<template>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="cursor-pointer table-toolbar-right-icon">
|
||||
<n-popover
|
||||
trigger="click"
|
||||
:width="230"
|
||||
class="toolbar-popover"
|
||||
placement="bottom-end"
|
||||
>
|
||||
<template #trigger>
|
||||
<n-icon size="18">
|
||||
<SettingOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
<template #header>
|
||||
<div class="table-toolbar-inner-popover-title">
|
||||
<n-space>
|
||||
<n-checkbox
|
||||
v-model:checked="checkAll"
|
||||
@update:checked="onCheckAll"
|
||||
>列展示</n-checkbox
|
||||
>
|
||||
<n-checkbox
|
||||
v-model:checked="selection"
|
||||
@update:checked="onSelection"
|
||||
>勾选列</n-checkbox
|
||||
>
|
||||
<n-button
|
||||
text
|
||||
type="info"
|
||||
size="small"
|
||||
class="mt-1"
|
||||
@click="resetColumns"
|
||||
>重置</n-button
|
||||
>
|
||||
</n-space>
|
||||
</div>
|
||||
</template>
|
||||
<div class="table-toolbar-inner">
|
||||
<n-checkbox-group
|
||||
v-model:value="checkList"
|
||||
@update:value="onChange"
|
||||
>
|
||||
<Draggable
|
||||
v-model="columnsList"
|
||||
animation="300"
|
||||
item-key="key"
|
||||
filter=".no-draggable"
|
||||
:move="onMove"
|
||||
@end="draggableEnd"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<div class="table-toolbar-inner-checkbox">
|
||||
<span
|
||||
class="drag-icon"
|
||||
:class="{
|
||||
'drag-icon-hidden': element.draggable === false,
|
||||
}"
|
||||
>
|
||||
<n-icon size="18">
|
||||
<DragOutlined />
|
||||
</n-icon>
|
||||
</span>
|
||||
<n-checkbox :value="element.key" :label="element.title" />
|
||||
<div class="fixed-item">
|
||||
<n-tooltip trigger="hover" placement="bottom">
|
||||
<template #trigger>
|
||||
<n-icon
|
||||
size="18"
|
||||
:color="
|
||||
element.fixed === 'left' ? '#2080f0' : undefined
|
||||
"
|
||||
class="cursor-pointer"
|
||||
@click="fixedColumn(element, 'left')"
|
||||
>
|
||||
<VerticalRightOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
<span>固定到左侧</span>
|
||||
</n-tooltip>
|
||||
<n-divider vertical />
|
||||
<n-tooltip trigger="hover" placement="bottom">
|
||||
<template #trigger>
|
||||
<n-icon
|
||||
size="18"
|
||||
:color="
|
||||
element.fixed === 'right' ? '#2080f0' : undefined
|
||||
"
|
||||
class="cursor-pointer"
|
||||
@click="fixedColumn(element, 'right')"
|
||||
>
|
||||
<VerticalLeftOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
<span>固定到右侧</span>
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
</n-checkbox-group>
|
||||
</div>
|
||||
</n-popover>
|
||||
</div>
|
||||
</template>
|
||||
<span>列设置</span>
|
||||
</n-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
ref,
|
||||
defineComponent,
|
||||
reactive,
|
||||
unref,
|
||||
toRaw,
|
||||
computed,
|
||||
toRefs,
|
||||
watchEffect,
|
||||
} from "vue";
|
||||
import { useTableContext } from "../../hooks/useTableContext";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import {
|
||||
SettingOutlined,
|
||||
DragOutlined,
|
||||
VerticalRightOutlined,
|
||||
VerticalLeftOutlined,
|
||||
} from "@vicons/antd";
|
||||
import Draggable from "vuedraggable";
|
||||
|
||||
interface Options {
|
||||
title: string;
|
||||
key: string;
|
||||
fixed?: boolean | "left" | "right";
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: "ColumnSetting",
|
||||
components: {
|
||||
SettingOutlined,
|
||||
DragOutlined,
|
||||
Draggable,
|
||||
VerticalRightOutlined,
|
||||
VerticalLeftOutlined,
|
||||
},
|
||||
setup() {
|
||||
const table: any = useTableContext();
|
||||
const columnsList = ref<Options[]>([]);
|
||||
const cacheColumnsList = ref<Options[]>([]);
|
||||
|
||||
const state = reactive({
|
||||
selection: false,
|
||||
checkAll: true,
|
||||
checkList: [],
|
||||
defaultCheckList: [],
|
||||
});
|
||||
|
||||
const getSelection = computed(() => {
|
||||
return state.selection;
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
const columns = table.getColumns();
|
||||
if (columns.length) {
|
||||
init();
|
||||
}
|
||||
});
|
||||
|
||||
//初始化
|
||||
function init() {
|
||||
const columns: any[] = getColumns();
|
||||
const checkList: any = columns.map((item) => item.key);
|
||||
state.checkList = checkList;
|
||||
state.defaultCheckList = checkList;
|
||||
const newColumns = columns.filter(
|
||||
(item) => item.key != "action" && item.title != "操作"
|
||||
);
|
||||
if (!columnsList.value.length) {
|
||||
columnsList.value = cloneDeep(newColumns);
|
||||
cacheColumnsList.value = cloneDeep(newColumns);
|
||||
}
|
||||
}
|
||||
|
||||
//切换
|
||||
function onChange(checkList) {
|
||||
if (state.selection) {
|
||||
checkList.unshift("selection");
|
||||
}
|
||||
setColumns(checkList);
|
||||
}
|
||||
|
||||
//设置
|
||||
function setColumns(columns) {
|
||||
table.setColumns(columns);
|
||||
}
|
||||
|
||||
//获取
|
||||
function getColumns() {
|
||||
let newRet: any[] = [];
|
||||
table.getColumns().forEach((item) => {
|
||||
newRet.push({ ...item });
|
||||
});
|
||||
return newRet;
|
||||
}
|
||||
|
||||
//重置
|
||||
function resetColumns() {
|
||||
state.checkList = [...state.defaultCheckList];
|
||||
state.checkAll = true;
|
||||
let cacheColumnsKeys: any[] = table.getCacheColumns();
|
||||
let newColumns = cacheColumnsKeys.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
fixed: undefined,
|
||||
};
|
||||
});
|
||||
setColumns(newColumns);
|
||||
columnsList.value = newColumns;
|
||||
}
|
||||
|
||||
//全选
|
||||
function onCheckAll(e) {
|
||||
let checkList = table.getCacheColumns(true);
|
||||
if (e) {
|
||||
setColumns(checkList);
|
||||
state.checkList = checkList;
|
||||
} else {
|
||||
setColumns([]);
|
||||
state.checkList = [];
|
||||
}
|
||||
}
|
||||
|
||||
//拖拽排序
|
||||
function draggableEnd() {
|
||||
const newColumns = toRaw(unref(columnsList));
|
||||
columnsList.value = newColumns;
|
||||
setColumns(newColumns);
|
||||
}
|
||||
|
||||
//勾选列
|
||||
function onSelection(e) {
|
||||
let checkList = table.getCacheColumns();
|
||||
if (e) {
|
||||
checkList.unshift({ type: "selection", key: "selection" });
|
||||
setColumns(checkList);
|
||||
} else {
|
||||
checkList.splice(0, 1);
|
||||
setColumns(checkList);
|
||||
}
|
||||
}
|
||||
|
||||
function onMove(e) {
|
||||
if (e.draggedContext.element.draggable === false) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
//固定
|
||||
function fixedColumn(item: any, fixed: any) {
|
||||
if (!state.checkList.includes(item.key)) return;
|
||||
let columns = getColumns();
|
||||
const isFixed = item.fixed === fixed ? undefined : fixed;
|
||||
let index = columns.findIndex((res) => res.key === item.key);
|
||||
if (index !== -1) {
|
||||
columns[index].fixed = isFixed;
|
||||
}
|
||||
table.setCacheColumnsField(item.key, { fixed: isFixed });
|
||||
columnsList.value[index].fixed = isFixed;
|
||||
setColumns(columns);
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
columnsList,
|
||||
onChange,
|
||||
onCheckAll,
|
||||
onSelection,
|
||||
onMove,
|
||||
resetColumns,
|
||||
fixedColumn,
|
||||
draggableEnd,
|
||||
getSelection,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.table-toolbar {
|
||||
&-inner-popover-title {
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
&-right {
|
||||
&-icon {
|
||||
margin-left: 12px;
|
||||
font-size: 16px;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
|
||||
:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-toolbar-inner {
|
||||
&-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
|
||||
&:hover {
|
||||
background: #e6f7ff;
|
||||
}
|
||||
|
||||
.drag-icon {
|
||||
display: inline-flex;
|
||||
margin-right: 8px;
|
||||
cursor: move;
|
||||
|
||||
&-hidden {
|
||||
cursor: default;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.ant-checkbox-wrapper {
|
||||
flex: 1;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-checkbox-dark {
|
||||
&:hover {
|
||||
background: hsl(0deg 0% 100% / 8%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-popover {
|
||||
.n-popover__content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
28
src/components/Table/src/const.ts
Normal file
@ -0,0 +1,28 @@
|
||||
// import componentSetting from "@/settings/componentSetting";
|
||||
|
||||
const table = {
|
||||
apiSetting: {
|
||||
// 当前页的字段名
|
||||
pageField: "current",
|
||||
// 每页数量字段名
|
||||
sizeField: "size",
|
||||
// 接口返回的数据字段名
|
||||
listField: "records",
|
||||
// 接口返回总页数字段名
|
||||
totalField: "pages",
|
||||
//总数字段名
|
||||
countField: "total",
|
||||
},
|
||||
//默认分页数量
|
||||
defaultPageSize: 10,
|
||||
//可切换每页数量集合
|
||||
pageSizes: [10, 20, 30, 40, 50],
|
||||
};
|
||||
|
||||
const { apiSetting, defaultPageSize, pageSizes } = table;
|
||||
|
||||
export const DEFAULTPAGESIZE = defaultPageSize;
|
||||
|
||||
export const APISETTING = apiSetting;
|
||||
|
||||
export const PAGESIZES = pageSizes;
|
||||
145
src/components/Table/src/hooks/useColumns.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import { ref, Ref, ComputedRef, unref, computed, watch, toRaw } from "vue";
|
||||
import type { BasicColumn, BasicTableProps } from "../types/table";
|
||||
import { isEqual, cloneDeep } from "lodash-es";
|
||||
import { isArray, isString } from "@/utils";
|
||||
// import { usePermission } from "@/hooks/web/usePermission";
|
||||
// import { ActionItem } from "@/components/Table";
|
||||
// import { renderEditCell } from "../components/editable";
|
||||
// import { NTooltip, NIcon } from "naive-ui";
|
||||
// import { FormOutlined } from "@vicons/antd";
|
||||
|
||||
export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
|
||||
const columnsRef = ref(unref(propsRef).columns) as unknown as Ref<
|
||||
BasicColumn[]
|
||||
>;
|
||||
let cacheColumns = unref(propsRef).columns;
|
||||
|
||||
const getColumnsRef = computed(() => {
|
||||
const columns = cloneDeep(unref(columnsRef));
|
||||
|
||||
handleActionColumn(propsRef, columns);
|
||||
if (!columns) return [];
|
||||
return columns;
|
||||
});
|
||||
|
||||
// const { hasPermission } = usePermission();
|
||||
|
||||
// function isIfShow(action: ActionItem): boolean {
|
||||
// const ifShow = action.ifShow;
|
||||
|
||||
// let isIfShow = true;
|
||||
|
||||
// if (isBoolean(ifShow)) {
|
||||
// isIfShow = ifShow;
|
||||
// }
|
||||
// if (isFunction(ifShow)) {
|
||||
// isIfShow = ifShow(action);
|
||||
// }
|
||||
// return isIfShow;
|
||||
// }
|
||||
|
||||
// const renderTooltip = (trigger, content) => {
|
||||
// return h(NTooltip, null, {
|
||||
// trigger: () => trigger,
|
||||
// default: () => content,
|
||||
// });
|
||||
// };
|
||||
|
||||
const getPageColumns = computed(() => {
|
||||
const pageColumns = unref(getColumnsRef);
|
||||
const columns = cloneDeep(pageColumns);
|
||||
return columns;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => unref(propsRef).columns,
|
||||
(columns) => {
|
||||
columnsRef.value = columns;
|
||||
cacheColumns = columns;
|
||||
}
|
||||
);
|
||||
|
||||
function handleActionColumn(
|
||||
propsRef: ComputedRef<BasicTableProps>,
|
||||
columns: BasicColumn[]
|
||||
) {
|
||||
const { actionColumn } = unref(propsRef);
|
||||
if (!actionColumn) return;
|
||||
!columns.find((col) => col.key === "action") &&
|
||||
columns.push({
|
||||
...(actionColumn as any),
|
||||
});
|
||||
}
|
||||
|
||||
//设置
|
||||
function setColumns(columnList: string[]) {
|
||||
const columns: any[] = cloneDeep(columnList);
|
||||
if (!isArray(columns)) return;
|
||||
|
||||
if (!columns.length) {
|
||||
columnsRef.value = [];
|
||||
return;
|
||||
}
|
||||
const cacheKeys = cacheColumns.map((item) => item.key);
|
||||
//针对拖拽排序
|
||||
if (!isString(columns[0])) {
|
||||
columnsRef.value = columns;
|
||||
} else {
|
||||
const newColumns: any[] = [];
|
||||
cacheColumns.forEach((item) => {
|
||||
if (columnList.includes(item.key)) {
|
||||
newColumns.push({ ...item });
|
||||
}
|
||||
});
|
||||
if (!isEqual(cacheKeys, columns)) {
|
||||
newColumns.sort((prev, next) => {
|
||||
return cacheKeys.indexOf(prev.key) - cacheKeys.indexOf(next.key);
|
||||
});
|
||||
}
|
||||
columnsRef.value = newColumns;
|
||||
}
|
||||
}
|
||||
|
||||
//获取
|
||||
function getColumns(): BasicColumn[] {
|
||||
const columns = toRaw(unref(getColumnsRef));
|
||||
return columns.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
title: item.title,
|
||||
key: item.key,
|
||||
fixed: item.fixed || undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
//获取原始
|
||||
function getCacheColumns(isKey?: boolean): any[] {
|
||||
return isKey ? cacheColumns.map((item) => item.key) : cacheColumns;
|
||||
}
|
||||
|
||||
//更新原始数据单个字段
|
||||
function setCacheColumnsField(
|
||||
key: string | undefined,
|
||||
value: Partial<BasicColumn>
|
||||
) {
|
||||
if (!key || !value) {
|
||||
return;
|
||||
}
|
||||
cacheColumns.forEach((item) => {
|
||||
if (item.key === key) {
|
||||
Object.assign(item, value);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
getColumnsRef,
|
||||
getCacheColumns,
|
||||
setCacheColumnsField,
|
||||
setColumns,
|
||||
getColumns,
|
||||
getPageColumns,
|
||||
};
|
||||
}
|
||||
157
src/components/Table/src/hooks/useDataSource.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import type { BasicTableProps } from "../types/table";
|
||||
import type { PaginationProps } from "../types/pagination";
|
||||
import { isBoolean, isFunction } from "@/utils/is";
|
||||
import { APISETTING } from "../const";
|
||||
|
||||
export function useDataSource(
|
||||
propsRef: ComputedRef<BasicTableProps>,
|
||||
{ getPaginationInfo, setPagination, setLoading, tableData },
|
||||
emit
|
||||
) {
|
||||
const dataSourceRef = ref<any[]>([]);
|
||||
|
||||
watchEffect(() => {
|
||||
tableData.value = unref(dataSourceRef);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => unref(propsRef).dataSource,
|
||||
() => {
|
||||
const { dataSource }: any = unref(propsRef);
|
||||
dataSource && (dataSourceRef.value = dataSource);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const getRowKey = computed(() => {
|
||||
const { rowKey }: any = unref(propsRef);
|
||||
return rowKey
|
||||
? rowKey
|
||||
: () => {
|
||||
return "key";
|
||||
};
|
||||
});
|
||||
|
||||
const getDataSourceRef = computed(() => {
|
||||
const dataSource = unref(dataSourceRef);
|
||||
if (!dataSource || dataSource.length === 0) {
|
||||
return unref(dataSourceRef);
|
||||
}
|
||||
return unref(dataSourceRef);
|
||||
});
|
||||
|
||||
async function fetch(opt?: { [x: string]: any } | undefined) {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { request, pagination, beforeRequest, afterRequest }: any =
|
||||
unref(propsRef);
|
||||
if (!request) return;
|
||||
//组装分页信息
|
||||
const pageField = APISETTING.pageField;
|
||||
const sizeField = APISETTING.sizeField;
|
||||
const totalField = APISETTING.totalField;
|
||||
const listField = APISETTING.listField;
|
||||
const itemCount = APISETTING.countField;
|
||||
let pageParams = {};
|
||||
const { page = 1, pageSize = 10 } = unref(
|
||||
getPaginationInfo
|
||||
) as PaginationProps;
|
||||
|
||||
if (
|
||||
(isBoolean(pagination) && !pagination) ||
|
||||
isBoolean(getPaginationInfo)
|
||||
) {
|
||||
pageParams = {};
|
||||
} else {
|
||||
pageParams[pageField] = (opt && opt[pageField]) || page;
|
||||
pageParams[sizeField] = pageSize;
|
||||
}
|
||||
|
||||
let params = {
|
||||
...pageParams,
|
||||
// ...opt,
|
||||
};
|
||||
// console.log(opt);
|
||||
|
||||
if (beforeRequest && isFunction(beforeRequest)) {
|
||||
// The params parameter can be modified by outsiders
|
||||
params = (await beforeRequest(params)) || params;
|
||||
}
|
||||
const res = await request(params);
|
||||
const resultTotal = res[totalField];
|
||||
const currentPage = res[pageField];
|
||||
const total = res[itemCount];
|
||||
const results = res[listField] ? res[listField] : [];
|
||||
|
||||
// 如果数据异常,需获取正确的页码再次执行
|
||||
if (resultTotal) {
|
||||
const currentTotalPage = Math.ceil(total / pageSize);
|
||||
if (page > currentTotalPage) {
|
||||
setPagination({
|
||||
page: currentTotalPage,
|
||||
itemCount: total,
|
||||
});
|
||||
return await fetch(opt);
|
||||
}
|
||||
}
|
||||
let resultInfo = res[listField] ? res[listField] : [];
|
||||
if (afterRequest && isFunction(afterRequest)) {
|
||||
// can modify the data returned by the interface for processing
|
||||
resultInfo = (await afterRequest(resultInfo)) || resultInfo;
|
||||
}
|
||||
dataSourceRef.value = resultInfo;
|
||||
setPagination({
|
||||
page: currentPage,
|
||||
pageCount: resultTotal,
|
||||
itemCount: total,
|
||||
});
|
||||
if (opt && opt[pageField]) {
|
||||
setPagination({
|
||||
page: opt[pageField] || 1,
|
||||
});
|
||||
}
|
||||
emit("fetch-success", {
|
||||
items: unref(resultInfo),
|
||||
resultTotal,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
emit("fetch-error", error);
|
||||
dataSourceRef.value = [];
|
||||
setPagination({
|
||||
pageCount: 0,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
fetch();
|
||||
}, 16);
|
||||
});
|
||||
|
||||
function setTableData(values) {
|
||||
dataSourceRef.value = values;
|
||||
}
|
||||
|
||||
function getDataSource(): any[] {
|
||||
return getDataSourceRef.value;
|
||||
}
|
||||
|
||||
async function reload(opt?) {
|
||||
await fetch(opt);
|
||||
}
|
||||
|
||||
return {
|
||||
fetch,
|
||||
getRowKey,
|
||||
getDataSourceRef,
|
||||
getDataSource,
|
||||
setTableData,
|
||||
reload,
|
||||
};
|
||||
}
|
||||
21
src/components/Table/src/hooks/useLoading.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { ref, ComputedRef, unref, computed, watch } from "vue";
|
||||
import type { BasicTableProps } from "../types/table";
|
||||
|
||||
export function useLoading(props: ComputedRef<BasicTableProps>) {
|
||||
const loadingRef = ref(unref(props).loading);
|
||||
|
||||
watch(
|
||||
() => unref(props).loading,
|
||||
(loading) => {
|
||||
loadingRef.value = loading;
|
||||
}
|
||||
);
|
||||
|
||||
const getLoading = computed(() => unref(loadingRef));
|
||||
|
||||
function setLoading(loading: boolean) {
|
||||
loadingRef.value = loading;
|
||||
}
|
||||
|
||||
return { getLoading, setLoading };
|
||||
}
|
||||
68
src/components/Table/src/hooks/usePagination.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import type { PaginationProps } from "../types/pagination";
|
||||
import type { BasicTableProps } from "../types/table";
|
||||
import { computed, unref, ref, ComputedRef, watch } from "vue";
|
||||
|
||||
import { isBoolean } from "@/utils/is";
|
||||
import { DEFAULTPAGESIZE, PAGESIZES } from "../const";
|
||||
|
||||
export function usePagination(refProps: ComputedRef<BasicTableProps>) {
|
||||
const configRef = ref<PaginationProps>({});
|
||||
const show = ref(true);
|
||||
|
||||
watch(
|
||||
() => unref(refProps).pagination,
|
||||
(pagination) => {
|
||||
if (!isBoolean(pagination) && pagination) {
|
||||
configRef.value = {
|
||||
...unref(configRef),
|
||||
...(pagination ?? {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const getPaginationInfo = computed((): PaginationProps | boolean => {
|
||||
const { pagination } = unref(refProps);
|
||||
if (!unref(show) || (isBoolean(pagination) && !pagination)) {
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
page: 1, //当前页
|
||||
pageSize: DEFAULTPAGESIZE, //分页大小
|
||||
pageSizes: PAGESIZES, // 每页条数
|
||||
showSizePicker: true,
|
||||
showQuickJumper: true,
|
||||
prefix: (pagingInfo) => `共 ${pagingInfo.itemCount} 条`, // 不需要可以通过 pagination 重置或者删除
|
||||
...(isBoolean(pagination) ? {} : pagination),
|
||||
...unref(configRef),
|
||||
};
|
||||
});
|
||||
|
||||
function setPagination(info: Partial<PaginationProps>) {
|
||||
const paginationInfo = unref(getPaginationInfo);
|
||||
configRef.value = {
|
||||
...(!isBoolean(paginationInfo) ? paginationInfo : {}),
|
||||
...info,
|
||||
};
|
||||
}
|
||||
|
||||
function getPagination() {
|
||||
return unref(getPaginationInfo);
|
||||
}
|
||||
|
||||
function getShowPagination() {
|
||||
return unref(show);
|
||||
}
|
||||
|
||||
async function setShowPagination(flag: boolean) {
|
||||
show.value = flag;
|
||||
}
|
||||
|
||||
return {
|
||||
getPagination,
|
||||
getPaginationInfo,
|
||||
setShowPagination,
|
||||
getShowPagination,
|
||||
setPagination,
|
||||
};
|
||||
}
|
||||
22
src/components/Table/src/hooks/useTableContext.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Ref } from "vue";
|
||||
import type { BasicTableProps, TableActionType } from "../types/table";
|
||||
import { provide, inject, ComputedRef } from "vue";
|
||||
|
||||
const key = Symbol("s-table");
|
||||
|
||||
type Instance = TableActionType & {
|
||||
wrapRef: Ref<Nullable<HTMLElement>>;
|
||||
getBindValues: ComputedRef<any>;
|
||||
};
|
||||
|
||||
type RetInstance = Omit<Instance, "getBindValues"> & {
|
||||
getBindValues: ComputedRef<BasicTableProps>;
|
||||
};
|
||||
|
||||
export function createTableContext(instance: Instance) {
|
||||
provide(key, instance);
|
||||
}
|
||||
|
||||
export function useTableContext(): RetInstance {
|
||||
return inject(key) as RetInstance;
|
||||
}
|
||||
59
src/components/Table/src/props.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import type { PropType } from "vue";
|
||||
import { propTypes } from "@/utils/propTypes";
|
||||
import { BasicColumn } from "./types/table";
|
||||
import { NDataTable } from "naive-ui";
|
||||
export const basicProps = {
|
||||
...NDataTable.props, // 这里继承原 UI 组件的 props
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
titleTooltip: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "medium",
|
||||
},
|
||||
dataSource: {
|
||||
type: [Object],
|
||||
default: () => [],
|
||||
},
|
||||
columns: {
|
||||
type: [Array] as PropType<BasicColumn[]>,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
beforeRequest: {
|
||||
type: Function as PropType<(...arg: any[]) => void | Promise<any>>,
|
||||
default: null,
|
||||
},
|
||||
request: {
|
||||
type: Function as PropType<(...arg: any[]) => Promise<any>>,
|
||||
default: null,
|
||||
},
|
||||
afterRequest: {
|
||||
type: Function as PropType<(...arg: any[]) => void | Promise<any>>,
|
||||
default: null,
|
||||
},
|
||||
rowKey: {
|
||||
type: [String, Function] as PropType<string | ((record: any) => string)>,
|
||||
default: undefined,
|
||||
},
|
||||
pagination: {
|
||||
type: [Object, Boolean],
|
||||
default: () => {},
|
||||
},
|
||||
//废弃
|
||||
showPagination: {
|
||||
type: [String, Boolean],
|
||||
default: "auto",
|
||||
},
|
||||
actionColumn: {
|
||||
type: Object as PropType<BasicColumn>,
|
||||
default: null,
|
||||
},
|
||||
canResize: propTypes.bool.def(true),
|
||||
resizeHeightOffset: propTypes.number.def(0),
|
||||
};
|
||||
9
src/components/Table/src/types/componentType.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export type ComponentType =
|
||||
| "NInput"
|
||||
| "NInputNumber"
|
||||
| "NSelect"
|
||||
| "NCheckbox"
|
||||
| "NSwitch"
|
||||
| "NDatePicker"
|
||||
| "NTimePicker"
|
||||
| "NCascader";
|
||||
10
src/components/Table/src/types/pagination.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export interface PaginationProps {
|
||||
page?: number; //<2F>ܿ<EFBFBD>ģʽ<C4A3>µĵ<C2B5>ǰҳ
|
||||
itemCount?: number; //<2F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
pageCount?: number; //<2F><>ҳ<EFBFBD><D2B3>
|
||||
pageSize?: number; //<2F>ܿ<EFBFBD>ģʽ<C4A3>µķ<C2B5>ҳ<EFBFBD><D2B3>С
|
||||
pageSizes?: number[]; //ÿҳ<C3BF><D2B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD>Զ<EFBFBD><D4B6><EFBFBD>
|
||||
showSizePicker?: boolean; //<2F>Ƿ<EFBFBD><C7B7><EFBFBD>ʾÿҳ<C3BF><D2B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѡ<EFBFBD><D1A1><EFBFBD><EFBFBD>
|
||||
showQuickJumper?: boolean; //<2F>Ƿ<EFBFBD><C7B7><EFBFBD>ʾ<EFBFBD><CABE><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ת
|
||||
prefix?: any; //<2F><>ҳǰ
|
||||
}
|
||||
41
src/components/Table/src/types/table.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import type {
|
||||
InternalRowData,
|
||||
TableBaseColumn,
|
||||
} from "naive-ui/lib/data-table/src/interface";
|
||||
import { ComponentType } from "./componentType";
|
||||
export interface BasicColumn<T = InternalRowData> extends TableBaseColumn<T> {
|
||||
//编辑表格
|
||||
edit?: boolean;
|
||||
editRow?: boolean;
|
||||
editable?: boolean;
|
||||
editComponent?: ComponentType;
|
||||
editComponentProps?: any;
|
||||
editRule?: boolean | ((text: string, record: any) => Promise<string>);
|
||||
editValueMap?: (value: any) => string;
|
||||
onEditRow?: () => void;
|
||||
// 权限编码控制是否显示
|
||||
auth?: string[];
|
||||
// 业务控制是否显示
|
||||
ifShow?: boolean | ((column: BasicColumn) => boolean);
|
||||
// 控制是否支持拖拽,默认支持
|
||||
draggable?: boolean;
|
||||
}
|
||||
|
||||
export interface TableActionType {
|
||||
reload: (opt) => Promise<void>;
|
||||
emit?: any;
|
||||
getColumns: (opt?) => BasicColumn[];
|
||||
setColumns: (columns: BasicColumn[] | string[]) => void;
|
||||
}
|
||||
|
||||
export interface BasicTableProps {
|
||||
title?: string;
|
||||
dataSource: Function;
|
||||
columns: any[];
|
||||
pagination: object;
|
||||
showPagination: boolean;
|
||||
actionColumn: any[];
|
||||
canResize: boolean;
|
||||
resizeHeightOffset: number;
|
||||
loading: boolean;
|
||||
}
|
||||
28
src/components/Table/src/types/tableAction.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { NButton } from "naive-ui";
|
||||
import type { Component } from "vue";
|
||||
// import { PermissionsEnum } from "@/enums/permissionsEnum";
|
||||
import { Fn } from "@vueuse/core";
|
||||
export interface ActionItem extends Partial<InstanceType<typeof NButton>> {
|
||||
onClick?: Fn;
|
||||
label?: string;
|
||||
type?: "success" | "error" | "warning" | "info" | "primary" | "default";
|
||||
// 设定 color 后会覆盖 type 的样式
|
||||
color?: string;
|
||||
icon?: Component;
|
||||
popConfirm?: PopConfirm;
|
||||
disabled?: boolean;
|
||||
divider?: boolean;
|
||||
// 权限编码控制是否显示
|
||||
// auth?: PermissionsEnum | PermissionsEnum[] | string | string[];
|
||||
// 业务控制是否显示
|
||||
ifShow?: boolean | ((action: ActionItem) => boolean);
|
||||
}
|
||||
|
||||
export interface PopConfirm {
|
||||
title: string;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
confirm: Fn;
|
||||
cancel?: Fn;
|
||||
icon?: Component;
|
||||
}
|
||||
97
src/directives/clickOutside.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { on, isServer } from "@/utils";
|
||||
// import { isServer } from "@/utils";
|
||||
import type {
|
||||
ComponentPublicInstance,
|
||||
DirectiveBinding,
|
||||
ObjectDirective,
|
||||
} from "vue";
|
||||
|
||||
type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void;
|
||||
|
||||
type FlushList = Map<
|
||||
HTMLElement,
|
||||
{
|
||||
documentHandler: DocumentHandler;
|
||||
bindingFn: (...args: unknown[]) => unknown;
|
||||
}
|
||||
>;
|
||||
|
||||
const nodeList: FlushList = new Map();
|
||||
|
||||
let startClick: MouseEvent;
|
||||
|
||||
if (!isServer) {
|
||||
on(document, "mousedown", (e: MouseEvent) => (startClick = e));
|
||||
on(document, "mouseup", (e: MouseEvent) => {
|
||||
for (const { documentHandler } of nodeList.values()) {
|
||||
documentHandler(e, startClick);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createDocumentHandler(
|
||||
el: HTMLElement,
|
||||
binding: DirectiveBinding
|
||||
): DocumentHandler {
|
||||
let excludes: HTMLElement[] = [];
|
||||
if (Array.isArray(binding.arg)) {
|
||||
excludes = binding.arg;
|
||||
} else {
|
||||
// due to current implementation on binding type is wrong the type casting is necessary here
|
||||
excludes.push(binding.arg as unknown as HTMLElement);
|
||||
}
|
||||
return function (mouseup, mousedown) {
|
||||
const popperRef = (
|
||||
binding.instance as ComponentPublicInstance<{
|
||||
popperRef: Nullable<HTMLElement>;
|
||||
}>
|
||||
).popperRef;
|
||||
const mouseUpTarget = mouseup.target as Node;
|
||||
const mouseDownTarget = mousedown.target as Node;
|
||||
const isBound = !binding || !binding.instance;
|
||||
const isTargetExists = !mouseUpTarget || !mouseDownTarget;
|
||||
const isContainedByEl =
|
||||
el.contains(mouseUpTarget) || el.contains(mouseDownTarget);
|
||||
const isSelf = el === mouseUpTarget;
|
||||
|
||||
const isTargetExcluded =
|
||||
(excludes.length &&
|
||||
excludes.some((item) => item?.contains(mouseUpTarget))) ||
|
||||
(excludes.length && excludes.includes(mouseDownTarget as HTMLElement));
|
||||
const isContainedByPopper =
|
||||
popperRef &&
|
||||
(popperRef.contains(mouseUpTarget) ||
|
||||
popperRef.contains(mouseDownTarget));
|
||||
if (
|
||||
isBound ||
|
||||
isTargetExists ||
|
||||
isContainedByEl ||
|
||||
isSelf ||
|
||||
isTargetExcluded ||
|
||||
isContainedByPopper
|
||||
) {
|
||||
return;
|
||||
}
|
||||
binding.value();
|
||||
};
|
||||
}
|
||||
|
||||
const ClickOutside: ObjectDirective = {
|
||||
beforeMount(el, binding) {
|
||||
nodeList.set(el, {
|
||||
documentHandler: createDocumentHandler(el, binding),
|
||||
bindingFn: binding.value,
|
||||
});
|
||||
},
|
||||
updated(el, binding) {
|
||||
nodeList.set(el, {
|
||||
documentHandler: createDocumentHandler(el, binding),
|
||||
bindingFn: binding.value,
|
||||
});
|
||||
},
|
||||
unmounted(el) {
|
||||
nodeList.delete(el);
|
||||
},
|
||||
};
|
||||
|
||||
export default ClickOutside;
|
||||
34
src/directives/copy.ts
Normal file
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* v-copy
|
||||
* 复制某个值至剪贴板
|
||||
* 接收参数:string类型/Ref<string>类型/Reactive<string>类型
|
||||
*/
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
interface ElType extends HTMLElement {
|
||||
copyData: string | number;
|
||||
__handleClick__: any;
|
||||
}
|
||||
const copy: Directive = {
|
||||
mounted(el: ElType, binding: DirectiveBinding) {
|
||||
el.copyData = binding.value;
|
||||
el.addEventListener("click", handleClick);
|
||||
},
|
||||
updated(el: ElType, binding: DirectiveBinding) {
|
||||
el.copyData = binding.value;
|
||||
},
|
||||
beforeUnmount(el: ElType) {
|
||||
el.removeEventListener("click", el.__handleClick__);
|
||||
},
|
||||
};
|
||||
function handleClick(this: any) {
|
||||
const input = document.createElement("input");
|
||||
input.value = this.copyData.toLocaleString();
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand("Copy");
|
||||
document.body.removeChild(input);
|
||||
console.log("复制成功", this.copyData);
|
||||
}
|
||||
|
||||
export default copy;
|
||||
31
src/directives/debounce.ts
Normal file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* v-debounce
|
||||
* 按钮防抖指令,可自行扩展至input
|
||||
* 接收参数:function类型
|
||||
*/
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
interface ElType extends HTMLElement {
|
||||
__handleClick__: () => any;
|
||||
}
|
||||
const debounce: Directive = {
|
||||
mounted(el: ElType, binding: DirectiveBinding) {
|
||||
if (typeof binding.value !== "function") {
|
||||
throw "callback must be a function";
|
||||
}
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
el.__handleClick__ = function () {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
binding.value();
|
||||
}, 500);
|
||||
};
|
||||
el.addEventListener("click", el.__handleClick__);
|
||||
},
|
||||
beforeUnmount(el: ElType) {
|
||||
el.removeEventListener("click", el.__handleClick__);
|
||||
},
|
||||
};
|
||||
|
||||
export default debounce;
|
||||
49
src/directives/draggable.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
需求:实现一个拖拽指令,可在父元素区域任意拖拽元素。
|
||||
|
||||
思路:
|
||||
1、设置需要拖拽的元素为absolute,其父元素为relative。
|
||||
2、鼠标按下(onmousedown)时记录目标元素当前的 left 和 top 值。
|
||||
3、鼠标移动(onmousemove)时计算每次移动的横向距离和纵向距离的变化值,并改变元素的 left 和 top 值
|
||||
4、鼠标松开(onmouseup)时完成一次拖拽
|
||||
|
||||
使用:在 Dom 上加上 v-draggable 即可
|
||||
<div class="dialog-model" v-draggable></div>
|
||||
*/
|
||||
import type { Directive } from "vue";
|
||||
interface ElType extends HTMLElement {
|
||||
parentNode: any;
|
||||
}
|
||||
const draggable: Directive = {
|
||||
mounted: function (el: ElType) {
|
||||
el.style.cursor = "move";
|
||||
el.style.position = "absolute";
|
||||
el.onmousedown = function (e) {
|
||||
const disX = e.pageX - el.offsetLeft;
|
||||
const disY = e.pageY - el.offsetTop;
|
||||
document.onmousemove = function (e) {
|
||||
let x = e.pageX - disX;
|
||||
let y = e.pageY - disY;
|
||||
const maxX = el.parentNode.offsetWidth - el.offsetWidth;
|
||||
const maxY = el.parentNode.offsetHeight - el.offsetHeight;
|
||||
if (x < 0) {
|
||||
x = 0;
|
||||
} else if (x > maxX) {
|
||||
x = maxX;
|
||||
}
|
||||
|
||||
if (y < 0) {
|
||||
y = 0;
|
||||
} else if (y > maxY) {
|
||||
y = maxY;
|
||||
}
|
||||
el.style.left = x + "px";
|
||||
el.style.top = y + "px";
|
||||
};
|
||||
document.onmouseup = function () {
|
||||
document.onmousemove = document.onmouseup = null;
|
||||
};
|
||||
};
|
||||
},
|
||||
};
|
||||
export default draggable;
|
||||
49
src/directives/longpress.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* v-longpress
|
||||
* 长按指令,长按时触发事件
|
||||
*/
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
const directive: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||
if (typeof binding.value !== "function") {
|
||||
throw "callback must be a function";
|
||||
}
|
||||
// 定义变量
|
||||
let pressTimer: any = null;
|
||||
// 创建计时器( 2秒后执行函数 )
|
||||
const start = (e: any) => {
|
||||
if (e.button) {
|
||||
if (e.type === "click" && e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (pressTimer === null) {
|
||||
pressTimer = setTimeout(() => {
|
||||
handler(e);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
// 取消计时器
|
||||
const cancel = () => {
|
||||
if (pressTimer !== null) {
|
||||
clearTimeout(pressTimer);
|
||||
pressTimer = null;
|
||||
}
|
||||
};
|
||||
// 运行函数
|
||||
const handler = (e: MouseEvent | TouchEvent) => {
|
||||
binding.value(e);
|
||||
};
|
||||
// 添加事件监听器
|
||||
el.addEventListener("mousedown", start);
|
||||
el.addEventListener("touchstart", start);
|
||||
// 取消计时器
|
||||
el.addEventListener("click", cancel);
|
||||
el.addEventListener("mouseout", cancel);
|
||||
el.addEventListener("touchend", cancel);
|
||||
el.addEventListener("touchcancel", cancel);
|
||||
},
|
||||
};
|
||||
|
||||
export default directive;
|
||||
41
src/directives/throttle.ts
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
需求:防止按钮在短时间内被多次点击,使用节流函数限制规定时间内只能点击一次。
|
||||
|
||||
思路:
|
||||
1、第一次点击,立即调用方法并禁用按钮,等延迟结束再次激活按钮
|
||||
2、将需要触发的方法绑定在指令上
|
||||
|
||||
使用:给 Dom 加上 v-throttle 及回调函数即可
|
||||
<button v-throttle="debounceClick">节流提交</button>
|
||||
*/
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
interface ElType extends HTMLElement {
|
||||
__handleClick__: () => any;
|
||||
disabled: boolean;
|
||||
}
|
||||
const throttle: Directive = {
|
||||
mounted(el: ElType, binding: DirectiveBinding) {
|
||||
if (typeof binding.value !== "function") {
|
||||
throw "callback must be a function";
|
||||
}
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
el.__handleClick__ = function () {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
if (!el.disabled) {
|
||||
el.disabled = true;
|
||||
binding.value();
|
||||
timer = setTimeout(() => {
|
||||
el.disabled = false;
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
el.addEventListener("click", el.__handleClick__);
|
||||
},
|
||||
beforeUnmount(el: ElType) {
|
||||
el.removeEventListener("click", el.__handleClick__);
|
||||
},
|
||||
};
|
||||
|
||||
export default throttle;
|
||||
164
src/enums/httpEnum.ts
Normal file
@ -0,0 +1,164 @@
|
||||
/**
|
||||
* @description: 请求结果集
|
||||
*/
|
||||
export enum ResultEnum {
|
||||
DATA_SUCCESS = 0,
|
||||
SUCCESS = 2000,
|
||||
SERVER_ERROR = 500,
|
||||
SERVER_FORBIDDEN = 403,
|
||||
NOT_FOUND = 404,
|
||||
TIMEOUT = 10042,
|
||||
}
|
||||
|
||||
// 数据相关
|
||||
export enum RequestDataTypeEnum {
|
||||
// 静态数据
|
||||
STATIC = 0,
|
||||
// 请求数据
|
||||
AJAX = 1,
|
||||
}
|
||||
|
||||
// 请求主体类型
|
||||
export enum RequestContentTypeEnum {
|
||||
// 普通请求
|
||||
DEFAULT = 0,
|
||||
// SQL请求
|
||||
SQL = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 请求方法
|
||||
*/
|
||||
export enum RequestHttpEnum {
|
||||
GET = "get",
|
||||
POST = "post",
|
||||
PATCH = "patch",
|
||||
PUT = "put",
|
||||
DELETE = "delete",
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 请求间隔
|
||||
*/
|
||||
export enum RequestHttpIntervalEnum {
|
||||
// 秒
|
||||
SECOND = "second",
|
||||
// 分
|
||||
MINUTE = "minute",
|
||||
// 时
|
||||
HOUR = "hour",
|
||||
// 天
|
||||
DAY = "day",
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 请求间隔名称
|
||||
*/
|
||||
export const SelectHttpTimeNameObj = {
|
||||
[RequestHttpIntervalEnum.SECOND]: "秒",
|
||||
[RequestHttpIntervalEnum.MINUTE]: "分",
|
||||
[RequestHttpIntervalEnum.HOUR]: "时",
|
||||
[RequestHttpIntervalEnum.DAY]: "天",
|
||||
};
|
||||
|
||||
/**
|
||||
* @description: 请求头部类型
|
||||
*/
|
||||
export enum RequestBodyEnum {
|
||||
NONE = "none",
|
||||
FORM_DATA = "form-data",
|
||||
X_WWW_FORM_URLENCODED = "x-www-form-urlencoded",
|
||||
JSON = "json",
|
||||
XML = "xml",
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 请求头部类型数组
|
||||
*/
|
||||
export const RequestBodyEnumList = [
|
||||
RequestBodyEnum.NONE,
|
||||
RequestBodyEnum.FORM_DATA,
|
||||
RequestBodyEnum.X_WWW_FORM_URLENCODED,
|
||||
RequestBodyEnum.JSON,
|
||||
RequestBodyEnum.XML,
|
||||
];
|
||||
|
||||
/**
|
||||
* @description: 请求参数类型
|
||||
*/
|
||||
export enum RequestParamsTypeEnum {
|
||||
PARAMS = "Params",
|
||||
BODY = "Body",
|
||||
HEADER = "Header",
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 请求参数主体
|
||||
*/
|
||||
export type RequestParamsObjType = {
|
||||
[T: string]: string;
|
||||
};
|
||||
export type RequestParams = {
|
||||
[RequestParamsTypeEnum.PARAMS]: RequestParamsObjType;
|
||||
[RequestParamsTypeEnum.HEADER]: RequestParamsObjType;
|
||||
[RequestParamsTypeEnum.BODY]: {
|
||||
[RequestBodyEnum.FORM_DATA]: RequestParamsObjType;
|
||||
[RequestBodyEnum.X_WWW_FORM_URLENCODED]: RequestParamsObjType;
|
||||
[RequestBodyEnum.JSON]: string;
|
||||
[RequestBodyEnum.XML]: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @description: 常用的contentTyp类型
|
||||
*/
|
||||
export enum ContentTypeEnum {
|
||||
// json
|
||||
JSON = "application/json;charset=UTF-8",
|
||||
// text
|
||||
TEXT = "text/plain;charset=UTF-8",
|
||||
// xml
|
||||
XML = "application/xml;charset=UTF-8",
|
||||
// application/x-www-form-urlencoded 一般配合qs
|
||||
FORM_URLENCODED = "application/x-www-form-urlencoded;charset=UTF-8",
|
||||
// form-data 上传
|
||||
FORM_DATA = "multipart/form-data;charset=UTF-8",
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// 请求公共类型
|
||||
type RequestPublicConfigType = {
|
||||
// 时间单位(时分秒)
|
||||
requestIntervalUnit: RequestHttpIntervalEnum;
|
||||
// 请求内容
|
||||
requestParams: RequestParams;
|
||||
};
|
||||
|
||||
// 全局的图表请求配置
|
||||
export interface RequestGlobalConfigType extends RequestPublicConfigType {
|
||||
// 组件定制轮询时间
|
||||
requestInterval: number;
|
||||
// 请求源地址
|
||||
requestOriginUrl?: string;
|
||||
}
|
||||
|
||||
// 单个图表请求配置
|
||||
export interface RequestConfigType extends RequestPublicConfigType {
|
||||
// 组件定制轮询时间
|
||||
requestInterval?: number;
|
||||
// 获取数据的方式
|
||||
requestDataType: RequestDataTypeEnum;
|
||||
// 请求方式 get/post/del/put/patch
|
||||
requestHttpType: RequestHttpEnum;
|
||||
// 源后续的 url
|
||||
requestUrl?: string;
|
||||
// 请求内容主体方式 普通/sql
|
||||
requestContentType: RequestContentTypeEnum;
|
||||
// 请求体类型
|
||||
requestParamsBodyType: RequestBodyEnum;
|
||||
// SQL 请求对象
|
||||
requestSQLContent: {
|
||||
sql: string;
|
||||
};
|
||||
}
|
||||
17
src/enums/pageEnum.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { ResultEnum } from "@/enums/httpEnum";
|
||||
|
||||
export enum PageEnum {
|
||||
// 错误
|
||||
ERROR_PAGE_NAME_403 = "ErrorPage403",
|
||||
ERROR_PAGE_NAME_404 = "ErrorPage404",
|
||||
BASE_LOGIN = "/login",
|
||||
REDIRECT = "/redirect",
|
||||
REDIRECT_NAME = "Redirect",
|
||||
// ERROR_PAGE_NAME_500 = "ErrorPage500",
|
||||
}
|
||||
|
||||
export const ErrorPageNameMap = new Map([
|
||||
[ResultEnum.NOT_FOUND, PageEnum.ERROR_PAGE_NAME_404],
|
||||
[ResultEnum.SERVER_FORBIDDEN, PageEnum.ERROR_PAGE_NAME_403],
|
||||
// [ResultEnum.SERVER_ERROR, PageEnum.ERROR_PAGE_NAME_500],
|
||||
]);
|
||||
6
src/enums/pluginEnum.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export enum DialogEnum {
|
||||
DELETE = "delete",
|
||||
WARNING = "warning",
|
||||
ERROR = "error",
|
||||
SUCCESS = "success",
|
||||
}
|
||||
8
src/enums/storageEnum.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export enum StorageEnum {
|
||||
// 全局设置
|
||||
ZS_SYSTEM_SETTING_STORE = "YSTEM_SETTING",
|
||||
// token 等信息
|
||||
ZS_ACCESS_TOKEN_STORE = "ACCESS_TOKEN",
|
||||
// 登录信息
|
||||
ZS_LOGIN_INFO_STORE = "LOGIN_INFO",
|
||||
}
|
||||
40
src/hooks/event/useWindowSizeFn.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { tryOnMounted, tryOnUnmounted } from "@vueuse/core";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
|
||||
interface WindowSizeOptions {
|
||||
once?: boolean;
|
||||
immediate?: boolean;
|
||||
listenerOptions?: AddEventListenerOptions | boolean;
|
||||
}
|
||||
|
||||
export function useWindowSizeFn(
|
||||
fn: any,
|
||||
wait = 150,
|
||||
options?: WindowSizeOptions
|
||||
) {
|
||||
let handler = () => {
|
||||
fn();
|
||||
};
|
||||
const handleSize = useDebounceFn(handler, wait);
|
||||
handler = handleSize;
|
||||
|
||||
const start = () => {
|
||||
if (options && options.immediate) {
|
||||
handler();
|
||||
}
|
||||
window.addEventListener("resize", handler);
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
window.removeEventListener("resize", handler);
|
||||
};
|
||||
|
||||
tryOnMounted(() => {
|
||||
start();
|
||||
});
|
||||
|
||||
tryOnUnmounted(() => {
|
||||
stop();
|
||||
});
|
||||
return [start, stop];
|
||||
}
|
||||