feat: init

This commit is contained in:
戴业伟 2024-01-05 17:28:54 +08:00
commit 7dc783a27a
161 changed files with 10068 additions and 0 deletions

12
.env.development Normal file
View 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
View File

@ -0,0 +1,9 @@
## 生产环境
VITE_APP_PORT = 3000
# API请求前缀
VITE_APP_BASE_API = '/zsqy'
# proxy代理配置
VITE_APP_API_URL = ""

14
.eslintignore Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit $1

4
.husky/pre-commit Normal file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint:lint-staged

11
.prettierignore Normal file
View File

@ -0,0 +1,11 @@
dist
node_modules
public
.husky
.vscode
.idea
*.sh
*.md
src/assets
stats.html

46
.prettierrc.cjs Normal file
View 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
View File

@ -0,0 +1,11 @@
dist
node_modules
public
.husky
.vscode
.idea
*.sh
*.md
src/assets
stats.html

43
.stylelintrc.cjs Normal file
View 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"],
},
],
},
};

1
README.md Normal file
View File

@ -0,0 +1 @@
# Vue 3 + TypeScript + Vite

93
commitlint.config.cjs Normal file
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
public/font/PingFang.otf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

27
public/font/index.css Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
public/logo2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

10
src/App.vue Normal file
View 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
View 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
View 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
View File

@ -0,0 +1 @@
export * from "./menu";

16
src/api/system/menu.ts Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

BIN
src/assets/images/404.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
src/assets/images/chart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

127
src/assets/images/login.svg Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
src/assets/images/ring.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
src/assets/images/title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

1
src/assets/svgs/403.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

1
src/assets/svgs/404.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

1
src/assets/svgs/500.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View 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%", //leftright
},
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>

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

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

View 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";

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

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

View 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;
}

View 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];
}

View 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);
}

View 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,
};
}

View 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 };
}

View 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,
},
};

View 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];

View 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";

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

View 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";

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

View 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 };

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

View File

@ -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,
}
),
}
);
};

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

View 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 "";
}

View 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
>;

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

View 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;

View 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,
};
}

View 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,
};
}

View 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 };
}

View 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,
};
}

View 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;
}

View 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),
};

View File

@ -0,0 +1,9 @@
export type ComponentType =
| "NInput"
| "NInputNumber"
| "NSelect"
| "NCheckbox"
| "NSwitch"
| "NDatePicker"
| "NTimePicker"
| "NCascader";

View 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><>ҳǰ׺
}

View 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;
}

View 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;
}

View 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
View 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;

View 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;

View File

@ -0,0 +1,49 @@
/*
1absoluterelative
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;

View 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;

View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
export enum DialogEnum {
DELETE = "delete",
WARNING = "warning",
ERROR = "error",
SUCCESS = "success",
}

8
src/enums/storageEnum.ts Normal file
View 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",
}

View 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];
}

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