提交 0b9a11a0 authored 作者: hejie's avatar hejie

refactor: copyEMS系统功能

上级 f8bb5cdd
{
"recommendations": [
"christian-kohler.path-intellisense",
"warmthsea.vscode-custom-code-color",
"vscode-icons-team.vscode-icons",
"davidanson.vscode-markdownlint",
"ms-azuretools.vscode-docker",
"stylelint.vscode-stylelint",
"bradlc.vscode-tailwindcss",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"lokalise.i18n-ally",
"redhat.vscode-yaml",
"csstools.postcss",
"mikestead.dotenv",
"eamodio.gitlens",
"antfu.iconify",
"Vue.volar"
]
}
{
"i18n-ally.localesPaths": [
"locales"
]
"editor.formatOnType": true,
"editor.formatOnSave": true,
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.tabSize": 2,
"editor.formatOnPaste": true,
"editor.guides.bracketPairs": "active",
"files.autoSave": "off",
"git.confirmSync": false,
"workbench.startupEditor": "newUntitledFile",
"editor.suggestSelection": "first",
"editor.acceptSuggestionOnCommitCharacter": false,
"css.lint.propertyIgnoredDueToDisplay": "ignore",
"editor.quickSuggestions": {
"other": true,
"comments": true,
"strings": true
},
"files.associations": {
"editor.snippetSuggestions": "top"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"i18n-ally.localesPaths": "locales",
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
"i18n-ally.namespace": true,
"i18n-ally.enabledParsers": [
"yaml",
"js"
],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": [
"vue"
],
"iconify.excludes": [
"el"
],
"vsmqtt.brokerProfiles": [
{
"name": "broker.emqx.io",
"host": "broker.emqx.io/mqtt",
"port": 1883,
"clientId": "vsmqtt_client_db34"
}
],
"vscodeCustomCodeColor.highlightValue": [
"v-loading",
"v-auth",
"v-copy",
"v-longpress",
"v-optimize",
"v-perms",
"v-ripple"
],
"vscodeCustomCodeColor.highlightValueColor": "#b392f0",
}
\ No newline at end of file
{
"Vue3.0快速生成模板": {
"scope": "vue",
"prefix": "Vue3.0",
"body": [
"<template>",
"\t<div>test</div>",
"</template>\n",
"<script lang='ts'>",
"export default {",
"\tsetup() {",
"\t\treturn {}",
"\t}",
"}",
"</script>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],
"description": "Vue3.0"
}
}
{
"Vue3.2+快速生成模板": {
"scope": "vue",
"prefix": "Vue3.2+",
"body": [
"<script setup lang='ts'>",
"</script>\n",
"<template>",
"\t<div>test</div>",
"</template>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],
"description": "Vue3.2+"
}
}
{
"Vue3.3+defineOptions快速生成模板": {
"scope": "vue",
"prefix": "Vue3.3+",
"body": [
"<script setup lang='ts'>",
"defineOptions({",
"\tname: ''",
"})",
"</script>\n",
"<template>",
"\t<div>test</div>",
"</template>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],
"description": "Vue3.3+defineOptions快速生成模板"
}
}
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
"body-leading-blank": [2, "always"],
"footer-leading-blank": [1, "always"],
"header-max-length": [2, "always", 108],
"subject-empty": [2, "never"],
"type-empty": [2, "never"],
"type-enum": [
2,
"always",
[
"feat", //新功能
"fix", //修复错误
"docs", //文档更新
"style", //代码风格 格式化代码、删除多余空格、添加注释等,不影响代码逻辑
"refactor", //代码重构 优化代码结构、重命名变量、简化逻辑等,不添加新功能或修复错误
"perf", //性能优化 提高代码性能(如减少内存占用、提高响应速度等)
"test", //测试代码更改 添加或更新测试用例、修复测试代码、更新测试框架
"build", //构建系统更改 更新项目的构建系统(如 Webpack、Vite、Babel 配置等)
"ci", //持续集成更改 更新 CI 配置文件(如 `.github/workflows`、`.gitlab-ci.yml` 等)或脚本
"chore", //项目维护 进行不属于上述类型的维护性更改,如更新依赖项、清理项目文件等
"revert", //撤销提交 撤销之前的提交
"wip", //工作进行中 表示正在进行的工作,通常用于未完成的功能开发
"workflow", //工作流程改进 优化项目的开发流程、工具链或工作方式
"types", //类型定义文件更改 更新项目的类型定义文件(如 TypeScript 的 `.d.ts` 文件)
"release" //发布相关进行版本发布或更新版本号
]
]
},
prompt: {
messages: {
type: "Select the type of change that you're committing:", //选择提交类型(如 feat、fix 等)。
scope: "Denote the SCOPE of this change (optional):", //填写提交范围,模块、组件、文件名、功能区域等(可选)。
customScope: "Denote the SCOPE of this change:",
subject: "Write a SHORT, IMPERATIVE tense description of the change:\n", //填写简短描述(必填)。
body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n', //填写详细描述(可选)。
// breaking:
// 'List any BREAKING CHANGES (optional). Use "|" to break new line:\n',
// footerPrefixsSelect:
// "Select the ISSUES type of changeList by this change (optional):",
// customFooterPrefixs: "Input ISSUES prefix:",
// footer: "List any ISSUES by this change. E.g.: #31, #34:\n",
confirmCommit: "Are you sure you want to proceed with the commit above?"
},
types: [
{ value: "feat", name: "feat: 🚀 A new feature", emoji: "🚀" },
{ value: "fix", name: "fix: 🧩 A bug fix", emoji: "🧩" },
{
value: "docs",
name: "docs: 📚 Documentation only changes",
emoji: "📚"
},
{
value: "style",
name: "style: 🎨 Changes that do not affect the meaning of the code",
emoji: "🎨"
},
{
value: "refactor",
name: "refactor: ♻️ A code change that neither fixes a bug nor adds a feature",
emoji: "♻️"
},
{
value: "perf",
name: "perf: ⚡️ A code change that improves performance",
emoji: "⚡️"
},
{
value: "test",
name: "test: ✅ Adding missing tests or correcting existing tests",
emoji: "✅"
},
{
value: "build",
name: "build: 📦️ Changes that affect the build system or external dependencies",
emoji: "📦️"
},
{
value: "ci",
name: "ci: 🎡 Changes to our CI configuration files and scripts",
emoji: "🎡"
},
{
value: "chore",
name: "chore: 🔨 Other changes that don't modify src or test files",
emoji: "🔨"
},
{
value: "revert",
name: "revert: ⏪️ Reverts a previous commit",
emoji: "⏪️"
},
{ value: "wip", name: "wip: 🕔 work in process", emoji: "🕔" },
{
value: "workflow",
name: "workflow: 📋 workflow improvements",
emoji: "📋"
},
{
value: "types",
name: "types: 🔰 type definition file changes",
emoji: "🔰"
}
],
useEmoji: true,
allowBreakingChanges: ["feat", "fix"]
}
};
......@@ -20,7 +20,8 @@
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
"prepare": "husky",
"preinstall": "npx only-allow pnpm",
"plop": "plop --plopfile ./plop-templates/plopfile.cjs"
"plop": "plop --plopfile ./plop-templates/plopfile.cjs",
"commit": "git-cz"
},
"keywords": [
"vue-pure-admin",
......@@ -69,6 +70,7 @@
"codemirror": "^5.65.19",
"codemirror-editor-vue3": "^2.8.0",
"cropperjs": "^1.6.2",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"deep-chat": "^2.1.1",
"echarts": "^5.6.0",
......@@ -138,7 +140,9 @@
"@vitejs/plugin-vue-jsx": "^4.1.2",
"boxen": "^8.0.1",
"code-inspector-plugin": "^0.20.7",
"commitizen": "^4.3.1",
"cssnano": "^7.0.6",
"cz-git": "^1.11.1",
"dagre": "^0.8.5",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.1",
......@@ -205,5 +209,10 @@
"typeit",
"vue-demi"
]
},
"config": {
"commitizen": {
"path": "./node_modules/cz-git"
}
}
}
......@@ -71,6 +71,9 @@ importers:
cropperjs:
specifier: ^1.6.2
version: 1.6.2
crypto-js:
specifier: ^4.2.0
version: 4.2.0
dayjs:
specifier: ^1.11.13
version: 1.11.13
......@@ -273,9 +276,15 @@ importers:
code-inspector-plugin:
specifier: ^0.20.7
version: 0.20.7
commitizen:
specifier: ^4.3.1
version: 4.3.1(@types/node@20.17.30)(typescript@5.8.3)
cssnano:
specifier: ^7.0.6
version: 7.0.6(postcss@8.5.3)
cz-git:
specifier: ^1.11.1
version: 1.11.1
dagre:
specifier: ^0.8.5
version: 0.8.5
......@@ -2147,6 +2156,10 @@ packages:
resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
engines: {node: '>=12'}
ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
......@@ -2197,6 +2210,10 @@ packages:
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
at-least-node@1.0.0:
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
engines: {node: '>= 4.0.0'}
autolinker@3.16.2:
resolution: {integrity: sha512-JiYl7j2Z19F9NdTmirENSUUIIL/9MytEWtmzhfmsKPCp9E+G35Y0UNCMoM9tFigxT59qSc8Ml2dlZXOCVTYwuA==}
......@@ -2298,6 +2315,10 @@ packages:
cacheable@1.8.10:
resolution: {integrity: sha512-0ZnbicB/N2R6uziva8l6O6BieBklArWyiGx4GkwAhLKhSHyQtRfM9T1nx7HHuHDKkYB/efJQhz3QJ6x/YqoZzA==}
cachedir@2.3.0:
resolution: {integrity: sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==}
engines: {node: '>=6'}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
......@@ -2338,6 +2359,10 @@ packages:
resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
engines: {node: '>=0.8'}
chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
chalk@4.1.1:
resolution: {integrity: sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==}
engines: {node: '>=10'}
......@@ -2405,6 +2430,10 @@ packages:
resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
engines: {node: '>=18'}
cli-width@3.0.0:
resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==}
engines: {node: '>= 10'}
cli-width@4.1.0:
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
engines: {node: '>= 12'}
......@@ -2450,10 +2479,16 @@ packages:
collect-v8-coverage@1.0.2:
resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==}
color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.3:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
......@@ -2478,6 +2513,11 @@ packages:
commist@1.1.0:
resolution: {integrity: sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==}
commitizen@4.3.1:
resolution: {integrity: sha512-gwAPAVTy/j5YcOOebcCRIijn+mSjWJC+IYKivTu6aG8Ei/scoXgfsMRnuAk6b0GRste2J4NGxVdMN3ZpfNaVaw==}
engines: {node: '>= 12'}
hasBin: true
compare-func@2.0.0:
resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==}
......@@ -2512,6 +2552,9 @@ packages:
resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==}
engines: {node: '>=16'}
conventional-commit-types@3.0.0:
resolution: {integrity: sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==}
conventional-commits-parser@5.0.0:
resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==}
engines: {node: '>=16'}
......@@ -2559,6 +2602,9 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
css-declaration-sorter@7.2.0:
resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==}
engines: {node: ^14 || ^16 || >=18}
......@@ -2628,6 +2674,14 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
cz-conventional-changelog@3.3.0:
resolution: {integrity: sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==}
engines: {node: '>= 10'}
cz-git@1.11.1:
resolution: {integrity: sha512-QIhpsX8blMydkGcSSlSb4VKvu4qHNtxAWeN0N3TWDfQw7VbVHMLlAwmLm/YxVk60KKPy42O5ihe7E0gosTG2kg==}
engines: {node: '>=v12.20.0'}
d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
......@@ -2747,6 +2801,10 @@ packages:
resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==}
engines: {node: '>=0.10.0'}
detect-indent@6.1.0:
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
engines: {node: '>=8'}
detect-libc@1.0.3:
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
engines: {node: '>=0.10'}
......@@ -2924,6 +2982,10 @@ packages:
escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
escape-string-regexp@2.0.0:
resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==}
engines: {node: '>=8'}
......@@ -3108,6 +3170,10 @@ packages:
picomatch:
optional: true
figures@3.2.0:
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
engines: {node: '>=8'}
file-entry-cache@10.0.8:
resolution: {integrity: sha512-FGXHpfmI4XyzbLd3HQ8cbUcsFGohJpZtmQRHr8z8FxxtCe2PcpgIlVLwIgunqjvRmXypBETvwhV4ptJizA+Y1Q==}
......@@ -3119,6 +3185,12 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
find-node-modules@2.1.3:
resolution: {integrity: sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==}
find-root@1.1.0:
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
......@@ -3131,6 +3203,10 @@ packages:
resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==}
engines: {node: '>=18'}
findup-sync@4.0.0:
resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==}
engines: {node: '>= 8'}
findup-sync@5.0.0:
resolution: {integrity: sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==}
engines: {node: '>= 10.13.0'}
......@@ -3193,6 +3269,10 @@ packages:
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
engines: {node: '>=12'}
fs-extra@9.1.0:
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
engines: {node: '>=10'}
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
......@@ -3334,6 +3414,10 @@ packages:
engines: {node: '>=0.4.7'}
hasBin: true
has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
......@@ -3479,6 +3563,10 @@ packages:
resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
inquirer@8.2.5:
resolution: {integrity: sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==}
engines: {node: '>=12.0.0'}
inquirer@9.3.7:
resolution: {integrity: sha512-LJKFHCSeIRq9hanN14IlOtPSTe3lNES7TYDTE2xxdAy1LS5rYphajK1qtwvj3YmQXvvk0U2Vbmcni8P9EIQW9w==}
engines: {node: '>=18'}
......@@ -3609,6 +3697,9 @@ packages:
is-url@1.2.4:
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
is-utf8@0.2.1:
resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==}
is-what@4.1.16:
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
engines: {node: '>=12.13'}
......@@ -4051,6 +4142,9 @@ packages:
lodash.kebabcase@4.1.1:
resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==}
lodash.map@4.6.0:
resolution: {integrity: sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==}
lodash.memoize@4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
......@@ -4096,6 +4190,10 @@ packages:
resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
engines: {node: '>=18'}
longest@2.0.1:
resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==}
engines: {node: '>=0.10.0'}
lower-case@2.0.2:
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
......@@ -4168,6 +4266,9 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
merge@2.1.1:
resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
......@@ -4206,6 +4307,9 @@ packages:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
minimist@1.2.7:
resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==}
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
......@@ -4247,6 +4351,9 @@ packages:
muggle-string@0.4.1:
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
mute-stream@0.0.8:
resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}
mute-stream@1.0.0:
resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
......@@ -4982,6 +5089,10 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
run-async@2.4.1:
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
engines: {node: '>=0.12.0'}
run-async@3.0.0:
resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==}
engines: {node: '>=0.12.0'}
......@@ -5307,6 +5418,10 @@ packages:
resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}
engines: {node: '>=16'}
supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
......@@ -7888,6 +8003,10 @@ snapshots:
ansi-regex@6.1.0: {}
ansi-styles@3.2.1:
dependencies:
color-convert: 1.9.3
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
......@@ -7923,6 +8042,8 @@ snapshots:
asynckit@0.4.0: {}
at-least-node@1.0.0: {}
autolinker@3.16.2:
dependencies:
tslib: 2.8.1
......@@ -8084,6 +8205,8 @@ snapshots:
hookified: 1.8.1
keyv: 5.3.2
cachedir@2.3.0: {}
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
......@@ -8127,6 +8250,12 @@ snapshots:
adler-32: 1.3.1
crc-32: 1.2.2
chalk@2.4.2:
dependencies:
ansi-styles: 3.2.1
escape-string-regexp: 1.0.5
supports-color: 5.5.0
chalk@4.1.1:
dependencies:
ansi-styles: 4.3.0
......@@ -8206,6 +8335,8 @@ snapshots:
slice-ansi: 5.0.0
string-width: 7.2.0
cli-width@3.0.0: {}
cli-width@4.1.0: {}
cliui@6.0.0:
......@@ -8263,10 +8394,16 @@ snapshots:
collect-v8-coverage@1.0.2: {}
color-convert@1.9.3:
dependencies:
color-name: 1.1.3
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.3: {}
color-name@1.1.4: {}
colord@2.9.3: {}
......@@ -8286,6 +8423,26 @@ snapshots:
leven: 2.1.0
minimist: 1.2.8
commitizen@4.3.1(@types/node@20.17.30)(typescript@5.8.3):
dependencies:
cachedir: 2.3.0
cz-conventional-changelog: 3.3.0(@types/node@20.17.30)(typescript@5.8.3)
dedent: 0.7.0
detect-indent: 6.1.0
find-node-modules: 2.1.3
find-root: 1.1.0
fs-extra: 9.1.0
glob: 7.2.3
inquirer: 8.2.5
is-utf8: 0.2.1
lodash: 4.17.21
minimist: 1.2.7
strip-bom: 4.0.0
strip-json-comments: 3.1.1
transitivePeerDependencies:
- '@types/node'
- typescript
compare-func@2.0.0:
dependencies:
array-ify: 1.0.0
......@@ -8323,6 +8480,8 @@ snapshots:
dependencies:
compare-func: 2.0.0
conventional-commit-types@3.0.0: {}
conventional-commits-parser@5.0.0:
dependencies:
JSONStream: 1.3.5
......@@ -8366,6 +8525,8 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
crypto-js@4.2.0: {}
css-declaration-sorter@7.2.0(postcss@8.5.3):
dependencies:
postcss: 8.5.3
......@@ -8457,6 +8618,22 @@ snapshots:
csstype@3.1.3: {}
cz-conventional-changelog@3.3.0(@types/node@20.17.30)(typescript@5.8.3):
dependencies:
chalk: 2.4.2
commitizen: 4.3.1(@types/node@20.17.30)(typescript@5.8.3)
conventional-commit-types: 3.0.0
lodash.map: 4.6.0
longest: 2.0.1
word-wrap: 1.2.5
optionalDependencies:
'@commitlint/load': 19.8.0(@types/node@20.17.30)(typescript@5.8.3)
transitivePeerDependencies:
- '@types/node'
- typescript
cz-git@1.11.1: {}
d3-color@3.1.0: {}
d3-dispatch@3.0.1: {}
......@@ -8567,6 +8744,8 @@ snapshots:
detect-file@1.0.0: {}
detect-indent@6.1.0: {}
detect-libc@1.0.3:
optional: true
......@@ -8808,6 +8987,8 @@ snapshots:
escape-html@1.0.3: {}
escape-string-regexp@1.0.5: {}
escape-string-regexp@2.0.0: {}
escape-string-regexp@4.0.0: {}
......@@ -9028,6 +9209,10 @@ snapshots:
optionalDependencies:
picomatch: 4.0.2
figures@3.2.0:
dependencies:
escape-string-regexp: 1.0.5
file-entry-cache@10.0.8:
dependencies:
flat-cache: 6.1.8
......@@ -9040,6 +9225,13 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
find-node-modules@2.1.3:
dependencies:
findup-sync: 4.0.0
merge: 2.1.1
find-root@1.1.0: {}
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
......@@ -9056,6 +9248,13 @@ snapshots:
path-exists: 5.0.0
unicorn-magic: 0.1.0
findup-sync@4.0.0:
dependencies:
detect-file: 1.0.0
is-glob: 4.0.3
micromatch: 4.0.8
resolve-dir: 1.0.1
findup-sync@5.0.0:
dependencies:
detect-file: 1.0.0
......@@ -9125,6 +9324,13 @@ snapshots:
jsonfile: 6.1.0
universalify: 2.0.1
fs-extra@9.1.0:
dependencies:
at-least-node: 1.0.0
graceful-fs: 4.2.11
jsonfile: 6.1.0
universalify: 2.0.1
fs.realpath@1.0.0: {}
fsevents@2.3.3:
......@@ -9295,6 +9501,8 @@ snapshots:
optionalDependencies:
uglify-js: 3.19.3
has-flag@3.0.0: {}
has-flag@4.0.0: {}
has-symbols@1.1.0: {}
......@@ -9421,6 +9629,24 @@ snapshots:
ini@4.1.1: {}
inquirer@8.2.5:
dependencies:
ansi-escapes: 4.3.2
chalk: 4.1.2
cli-cursor: 3.1.0
cli-width: 3.0.0
external-editor: 3.1.0
figures: 3.2.0
lodash: 4.17.21
mute-stream: 0.0.8
ora: 5.4.1
run-async: 2.4.1
rxjs: 7.8.2
string-width: 4.2.3
strip-ansi: 6.0.1
through: 2.3.8
wrap-ansi: 7.0.0
inquirer@9.3.7:
dependencies:
'@inquirer/figures': 1.0.11
......@@ -9521,6 +9747,8 @@ snapshots:
is-url@1.2.4: {}
is-utf8@0.2.1: {}
is-what@4.1.16: {}
is-windows@1.0.2: {}
......@@ -10181,6 +10409,8 @@ snapshots:
lodash.kebabcase@4.1.1: {}
lodash.map@4.6.0: {}
lodash.memoize@4.1.2: {}
lodash.merge@4.6.2: {}
......@@ -10221,6 +10451,8 @@ snapshots:
strip-ansi: 7.1.0
wrap-ansi: 9.0.0
longest@2.0.1: {}
lower-case@2.0.2:
dependencies:
tslib: 2.8.1
......@@ -10279,6 +10511,8 @@ snapshots:
merge2@1.4.1: {}
merge@2.1.1: {}
micromatch@4.0.8:
dependencies:
braces: 3.0.3
......@@ -10312,6 +10546,8 @@ snapshots:
dependencies:
brace-expansion: 2.0.1
minimist@1.2.7: {}
minimist@1.2.8: {}
minipass@7.1.2: {}
......@@ -10369,6 +10605,8 @@ snapshots:
muggle-string@0.4.1: {}
mute-stream@0.0.8: {}
mute-stream@1.0.0: {}
namespace-emitter@2.0.1: {}
......@@ -11098,6 +11336,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.39.0
fsevents: 2.3.3
run-async@2.4.1: {}
run-async@3.0.0: {}
run-parallel@1.2.0:
......@@ -11449,6 +11689,10 @@ snapshots:
dependencies:
copy-anything: 3.0.5
supports-color@5.5.0:
dependencies:
has-flag: 3.0.0
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
......
{
"Version": "6.0.0",
"Title": "统一后台权限管理系统",
"Title": "硬件设备管理系统",
"FixedHeader": true,
"HiddenSideBar": false,
"MultiTagsCache": false,
......
......@@ -53,6 +53,26 @@ export default defineComponent({
}
);
}
// (() => {
// function block() {
// if (
// window.outerHeight - window.innerHeight > 200 ||
// window.outerWidth - window.innerWidth > 200
// ) {
// document.body.innerHTML = "检测到非法调试,请关闭后刷新重试!";
// }
// setInterval(() => {
// (function () {
// return false;
// })
// ["constructor"]("debugger")
// ["call"]();
// }, 50);
// }
// try {
// block();
// } catch (err) {}
// })();
}
});
</script>
......@@ -3,25 +3,29 @@ import { http } from "@/utils/http";
type Result = {
success: boolean;
data?: Array<any>;
code?: string;
};
type ResultTable = {
success: boolean;
data?: {
/** 列表数据 */
list: Array<any>;
list?: Array<any>;
/** 总条目数 */
total?: number;
/** 每页显示条目个数 */
pageSize?: number;
/** 当前页数 */
currentPage?: number;
records?: Array<any>;
};
};
/** 获取系统管理-用户管理列表 */
export const getUserList = (data?: object) => {
return http.request<ResultTable>("post", "/user", { data });
return http.request<ResultTable>("post", "/api/user/find-user-list-by-page", {
data
});
};
/** 系统管理-用户管理-获取所有角色列表 */
......@@ -41,12 +45,60 @@ export const getRoleList = (data?: object) => {
/** 获取系统管理-菜单管理列表 */
export const getMenuList = (data?: object) => {
return http.request<Result>("post", "/menu", { data });
return http.request<ResultTable>("post", "/api/menu/find-menu-list-by-page", {
data
});
};
/** 新增菜单-菜单管理列表 */
export const addMenu = (data?: object) => {
return http.request<Result>("post", "/api/menu/add-menu", { data });
};
/** 修改菜单-菜单管理列表 */
export const updateMenu = (data?: object) => {
return http.request<Result>("post", "/api/menu/modify-menu", { data });
};
/** 删除菜单-菜单管理列表 */
export const deleteMenu = (data?: { id: number }) => {
return http.request<Result>("post", `/api/menu/delete-menu/${data.id}`);
};
// 根据角色ID获取菜单列表
export const getMenuListByRoleId = (roleId: number) => {
return http.request<Result>("post", `/api/menu/get-menu-by-role/${roleId}`);
};
// 给角色绑定菜单
export const bindMenuByRoleId = (data?: object) => {
return http.request<Result>("post", "/api/role/add-role-menu", { data });
};
/** 获取系统管理-部门管理列表 */
export const getDeptList = (data?: object) => {
return http.request<Result>("post", "/dept", { data });
return http.request<ResultTable>(
"post",
"/api/depart/get-depart-list-by-page",
{
data
}
);
};
/** 新增部门-部门管理列表 */
export const addDept = (data?: object) => {
return http.request<Result>("post", "/api/depart/add-depart", { data });
};
/** 修改部门-部门管理列表 */
export const updateDept = (data?: object) => {
return http.request<Result>("post", `/api/depart/modify-depart`, { data });
};
/** 删除部门-部门管理列表 */
export const deleteDept = (data?: { id: number }) => {
return http.request<Result>("post", `/api/depart/delete-depart/${data.id}`);
};
/** 获取系统监控-在线用户列表 */
......
import { http } from "@/utils/http";
//#region =============================== 登录模块对应接口 ====================================
/** 登录 */
export const getLogin = (data?: object) => {
return http.request<LoginResult>("post", "/api/auth/login", { data });
};
/** 获取验证码 */
export const getCaptcha = (time?: string) => {
return http.request<CaptchaResult>("get", "/api/auth/captchaNumber/" + time);
};
/** 刷新`token` */
export const refreshTokenApi = (data?: object) => {
return http.request<RefreshTokenResult>("post", "/refresh-token", { data });
};
/** 账户设置-个人信息 */
export const getMine = () => {
return http.request<UserInfoResult>("post", "/api/user/user-info");
};
/** 用户管理-重置密码 */
export const resetPassword = (data?: object) => {
return http.request<UserInfoResult>("post", "/api/user/reset-password", {
data
});
};
//#endregion
//#region =============================== 用户管理模块对应接口 ===============================
/** 用户管理-新增用户 */
export const addUser = (data?: object) => {
return http.request<UserInfoResult>("post", "/api/user/add-user", { data });
};
/** 用户管理-删除用户 */
export const deleteUser = (id: number | string) => {
return http.request<UserInfoResult>("post", `/api/user/delete-user/${id}`);
};
/** 用户管理-修改用户 */
export const updateUser = (data?: object) => {
return http.request<UserInfoResult>("post", "/api/user/modify-user", {
data
});
};
/** 获取系统管理-用户管理列表 */
export const getUserList = (data?: object) => {
return http.request<ResultTable>("post", "/api/user/find-user-list-by-page", {
data
});
};
/** 用户管理-获取用户详情 */
export const getUserDetail = (data?: object) => {
return http.request<UserInfoResult>("get", "/api/user/user-detail", {
data
});
};
/** 用户管理-给用户绑定上角色 */
export const addUserRole = (data?: object) => {
return http.request<UserInfoResult>("post", "/api/user/add-user-role", {
data
});
};
//#endregion
//#region =============================== 菜单管理模块对应接口 ===============================
/** 新增菜单-菜单管理列表 */
export const addMenu = (data?: object) => {
return http.request<Result>("post", "/api/menu/add-menu", { data });
};
/** 删除菜单-菜单管理列表 */
export const deleteMenu = (data?: { id: number }) => {
return http.request<Result>("post", `/api/menu/delete-menu/${data.id}`);
};
/** 修改菜单-菜单管理列表 */
export const updateMenu = (data?: object) => {
return http.request<Result>("post", "/api/menu/modify-menu", { data });
};
/** 获取系统管理-菜单管理列表 */
export const getMenuList = (data?: object) => {
return http.request<ResultTable>("post", "/api/menu/find-menu-list-by-page", {
data
});
};
//#endregion
//#region =============================== 部门管理模块对应接口 ===============================
/** 新增部门-部门管理列表 */
export const addDept = (data?: object) => {
return http.request<Result>("post", "/api/depart/add-depart", { data });
};
/** 删除部门-部门管理列表 */
export const deleteDept = (data?: { id: number }) => {
return http.request<Result>("post", `/api/depart/delete-depart/${data.id}`);
};
/** 修改部门-部门管理列表 */
export const updateDept = (data?: object) => {
return http.request<Result>("post", `/api/depart/modify-depart`, { data });
};
/** 获取系统管理-部门管理列表 */
export const getDeptList = (data?: object) => {
return http.request<ResultTable>(
"post",
"/api/depart/get-depart-list-by-page",
{
data
}
);
};
//#endregion
//#region =============================== 角色管理模块对应接口 ===============================
// 角色管理-添加角色
export const addRole = (data?: object) => {
return http.request<Result>("post", "/api/role/add-role", { data });
};
// 角色管理-删除角色
export const deleteRole = (id: string) => {
return http.request<Result>("post", `/api/role/delete-role/${id}`);
};
// 角色管理-修改角色
export const updateRole = (data?: object) => {
return http.request<Result>("post", "/api/role/modify-role", { data });
};
/** 获取系统管理-角色管理列表 */
export const getRoleList = (data?: object) => {
// return http.request<ResultTable>("post", "/role", { data });
return http.request<ResultTable>("post", "/api/role/find-role-list-by-page", {
data
});
};
/** 获取角色管理-权限-菜单权限 */
// export const getRoleMenu = (data?: object) => {
// return http.request<Result>("post", "/role-menu", { data });
// };
/** 获取角色管理-权限-菜单权限-根据角色 id 查对应菜单 */
// export const getRoleMenuIds = (data?: object) => {
// return http.request<Result>("post", "/role-menu-ids", { data });
// };
/** 获取系统管理-不分页查询角色管理列表 */
export const getRoleListNoPage = () => {
// return http.request<ResultTable>("post", "/role", { data });
return http.request<ResultTable>("post", "/api/role/get-role-list");
};
// 根据角色ID获取菜单列表
export const getMenuListByRoleId = (roleId: number) => {
return http.request<Result>("post", `/api/menu/get-menu-by-role/${roleId}`);
};
// 角色管理-根据用户id查询角色列表
export const getRoleListByUserId = (id: string | number) => {
return http.request<Result>("post", `/api/role/get-role-by-user/${id}`);
};
// 给角色绑定菜单
export const bindMenuByRoleId = (data?: object) => {
return http.request<Result>("post", "/api/role/add-role-menu", { data });
};
// 用户管理 - 根据用户id绑定角色
// export const bindRoleByUserId = (data?: object) => {
// return http.request<Result>("post", "/api/role/bind-role-by-user", { data });
// };
//#endregion
// ========================= 定义ts类型 =========================
type Result = {
success: boolean;
data?: Array<any>;
code?: string;
};
type ResultTable = {
success: boolean;
data?: {
/** 列表数据 */
list?: Array<any>;
[key: string]: any;
list: Array<any>;
/** 总条目数 */
total?: number;
/** 每页显示条目个数 */
pageSize?: number;
/** 当前页数 */
currentPage?: number;
records?: Array<any>;
};
};
/** 获取Systems列表 */
export const getSystemsList = (data?: object) => {
return http.request<Result>("post", "/get-systems-list", { data });
export type UserInfo = {
menuList: Array<any>;
/** 头像 */
avatar: string;
/** 用户名 */
username: string;
/** 昵称 */
nickname: string;
/** 邮箱 */
email: string;
/** 联系电话 */
phone: string;
/** 简介 */
description: string;
};
/** 创建Systems */
export const createSystems = (data?: object) => {
return http.request<Result>("post", "/create-systems", { data });
export type UserInfoResult = {
success: boolean;
data?: UserInfo;
code?: number | string;
};
export type LoginResult = {
status: number;
success: boolean;
data: {
token: string;
/** 头像 */
avatar: string;
/** 用户名 */
username: string;
/** 昵称 */
nickname: string;
/** 当前登录用户的角色 */
roles: Array<string>;
/** 按钮级别权限 */
permissions: Array<string>;
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */
expires: Date;
};
};
/** 更新Systems */
export const updateSystems = (data?: object) => {
return http.request<Result>("post", "/update-systems", { data });
export type RefreshTokenResult = {
success: boolean;
data: {
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */
expires: Date;
};
};
/** 删除Systems */
export const deleteSystems = (data?: { id: string }) => {
return http.request<Result>("post", "/delete-systems", { data });
export type CaptchaResult = {
code: string;
success: boolean;
data: string;
};
......@@ -4,7 +4,7 @@ export type UserResult = {
status: number;
success: boolean;
data: {
jwt: string;
token: string;
/** 头像 */
avatar: string;
/** 用户名 */
......@@ -27,6 +27,8 @@ export type UserResult = {
export type RefreshTokenResult = {
success: boolean;
data: {
/** `token` */
token?: string;
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
......@@ -54,7 +56,8 @@ export type UserInfo = {
export type UserInfoResult = {
success: boolean;
data: UserInfo;
data?: UserInfo;
code?: number | string;
};
type ResultTable = {
......@@ -78,7 +81,9 @@ export const getLogin = (data?: object) => {
/** 刷新`token` */
export const refreshTokenApi = (data?: object) => {
return http.request<RefreshTokenResult>("post", "/refresh-token", { data });
return http.request<RefreshTokenResult>("post", "/api/auth/refresh-token", {
data
});
};
/** 账户设置-个人信息 */
......@@ -90,3 +95,43 @@ export const getMine = () => {
export const getMineLogs = (data?: object) => {
return http.request<ResultTable>("get", "/mine-logs", { data });
};
/** 系统管理-用户管理-新增用户 */
export const addUser = (data?: object) => {
return http.request<UserInfoResult>("post", "/api/user/add-user", { data });
};
/** 系统管理-用户管理-修改用户 */
export const updateUser = (data?: object) => {
return http.request<UserInfoResult>("post", "/api/user/modify-user", {
data
});
};
/** 系统管理-用户管理-给用户绑定上角色 */
export const addUserRole = (data?: object) => {
return http.request<UserInfoResult>("post", "/api/user/add-user-role", {
data
});
};
/** 系统管理-用户管理-删除用户 */
export const deleteUser = (id: number | string) => {
return http.request<UserInfoResult>("post", `/api/user/delete-user/${id}`);
};
/** 系统管理-用户管理-重置密码 */
export const resetPassword = (data?: object) => {
return http.request<UserInfoResult>("post", "/api/user/reset-password", {
data
});
};
/** 系统管理-用户管理-获取用户列表 */
export const getUserList = (data?: object) => {
return http.request<ResultTable>("get", "/api/user/user-list", { data });
};
/** 系统管理-用户管理-获取用户详情 */
export const getUserDetail = (data?: object) => {
return http.request<UserInfoResult>("get", "/api/user/user-detail", {
data
});
};
import { ref, onMounted } from "vue";
import { getCaptcha } from "@/api/systems";
import { useUserStoreHook } from "@/store/modules/user";
/**
* 绘制图形验证码
* @param width - 图形宽度
......@@ -15,7 +16,9 @@ export const useImageVerify = (width = 120, height = 40) => {
function getImgCode() {
if (!domRef.value) return;
imgCode.value = draw(domRef.value, width, height);
draw(domRef.value, width, height).then(code => {
imgCode.value = code;
});
}
onMounted(() => {
......@@ -41,19 +44,25 @@ function randomColor(min: number, max: number) {
const b = randomNum(min, max);
return `rgb(${r},${g},${b})`;
}
function draw(dom: HTMLCanvasElement, width: number, height: number) {
function draw(
dom: HTMLCanvasElement,
width: number,
height: number
): Promise<string> {
return new Promise(resolve => {
let imgCode = "";
const NUMBER_STRING = "0123456789";
const now = new Date().getTime().toString();
getCaptcha(now).then(res => {
if (res.code === "0") {
const captchaCode = res.data;
useUserStoreHook().SET_VERIFYCODE(captchaCode);
useUserStoreHook().SET_CAPTCHATIME(now);
const ctx = dom.getContext("2d");
if (!ctx) return imgCode;
if (!ctx) return resolve(imgCode);
ctx.fillStyle = randomColor(180, 230);
ctx.fillRect(0, 0, width, height);
for (let i = 0; i < 4; i += 1) {
const text = NUMBER_STRING[randomNum(0, NUMBER_STRING.length)];
for (let i = 0; i < captchaCode.length; i += 1) {
const text = captchaCode[i];
imgCode += text;
const fontSize = randomNum(18, 41);
const deg = randomNum(-30, 30);
......@@ -81,5 +90,10 @@ function draw(dom: HTMLCanvasElement, width: number, height: number) {
ctx.fillStyle = randomColor(150, 200);
ctx.fill();
}
return imgCode;
resolve(imgCode);
} else {
resolve("");
}
});
});
}
......@@ -69,6 +69,9 @@ export default defineComponent({
function handleChange({ option, index }, event: Event) {
if (props.disabled || option.disabled) return;
event.preventDefault();
console.log("props.modelValue", props.modelValue);
console.log("props.modelValue.type", typeof props.modelValue);
console.log("handleChange", option, index);
isNumber(props.modelValue)
? emit("update:modelValue", index)
: (curIndex.value = index);
......
......@@ -26,10 +26,21 @@ const {
toggleSideBar,
toAccountSettings,
getDropdownItemStyle,
getDropdownItemClass
getDropdownItemClass,
goLink
} = useNav();
const { t, locale, translationCh, translationEn } = useTranslationLang();
const aa = 123;
// 定义外链数组
const urlList = ref([
{ name: "EMS", url: "https://pure-admin.github.io/vue-pure-admin/#/welcome" },
{
name: "数据大屏",
url: "https://pure-admin.github.io/vue-pure-admin/#/welcome"
},
{ name: "DBSM", url: "https://pure-admin.github.io/vue-pure-admin/#/welcome" }
]);
</script>
<template>
......@@ -87,6 +98,24 @@ const { t, locale, translationCh, translationEn } = useTranslationLang();
<LaySidebarFullScreen id="full-screen" />
<!-- 消息通知 -->
<LayNotice id="header-notice" />
<!-- 外链列表 -->
<!-- 退出登录 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover select-none">
<p class="dark:text-white">其它系统</p>
</span>
<template #dropdown>
<el-dropdown-menu class="logout">
<el-dropdown-item
v-for="item in urlList"
:key="item.url"
@click="goLink(item)"
>
{{ item.name }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 退出登录 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover select-none">
......
......@@ -147,6 +147,10 @@ export function useNav() {
return new URL("/logo.svg", import.meta.url).href;
}
function goLink(item) {
window.open(item.url, "__blank");
}
return {
title,
device,
......@@ -175,6 +179,7 @@ export function useNav() {
tooltipEffect,
toAccountSettings,
getDropdownItemStyle,
getDropdownItemClass
getDropdownItemClass,
goLink
};
}
......@@ -218,8 +218,7 @@ function initRouter() {
} else {
return new Promise(resolve => {
getMine().then(({ data }) => {
console.log("路由数据---:", data);
data.menuList.shift();
// console.log("路由数据---:", data);
const treeData = transformRoutes(data.menuList);
handleAsyncRoutes(cloneDeep(treeData));
......
......@@ -14,7 +14,13 @@ import {
refreshTokenApi
} from "@/api/user";
import { useMultiTagsStoreHook } from "./multiTags";
import { type DataInfo, setToken, removeToken, userKey } from "@/utils/auth";
import {
type DataInfo,
setToken,
removeToken,
userKey,
getToken
} from "@/utils/auth";
export const useUserStore = defineStore("pure-user", {
state: (): userType => ({
......@@ -37,8 +43,10 @@ export const useUserStore = defineStore("pure-user", {
isRemembered: false,
// 登录页的免登录存储几天,默认7天
loginDay: 7,
token: localStorage.getItem("jwt") || "",
refreshToken: localStorage.getItem("refreshToken") || ""
token: localStorage.getItem("token") || "",
refreshToken: localStorage.getItem("refreshToken") || "",
// 拿到验证码的时间戳
captchaTime: ""
}),
actions: {
/** 存储头像 */
......@@ -77,6 +85,10 @@ export const useUserStore = defineStore("pure-user", {
SET_LOGINDAY(value: number) {
this.loginDay = Number(value);
},
/** 设置登录时验证码时间戳 */
SET_CAPTCHATIME(value: string) {
this.captchaTime = value;
},
/** 登入 */
async loginByUsername(data) {
return new Promise<UserResult>((resolve, reject) => {
......@@ -106,7 +118,12 @@ export const useUserStore = defineStore("pure-user", {
refreshTokenApi(data)
.then(data => {
if (data) {
if (data.data.token) {
data.data.accessToken = data.data.token;
}
setToken(data.data);
console.log("data", data.data);
console.log(getToken());
resolve(data);
}
})
......
......@@ -49,4 +49,5 @@ export type userType = {
loginDay?: number;
token: string;
refreshToken: string;
captchaTime: string;
};
......@@ -19,6 +19,8 @@ export interface DataInfo<T> {
roles?: Array<string>;
/** 当前登录用户的按钮级别权限 */
permissions?: Array<string>;
/** 用于调用刷新accessToken的接口时所需的token */
token?: string;
}
export const userKey = "user-info";
......@@ -45,11 +47,11 @@ export function getToken(): DataInfo<number> {
* 将`accessToken`、`expires`、`refreshToken`这三条信息放在key值为authorized-token的cookie里(过期自动销毁)
* 将`avatar`、`username`、`nickname`、`roles`、`permissions`、`refreshToken`、`expires`这七条信息放在key值为`user-info`的localStorage里(利用`multipleTabsKey`当浏览器完全关闭后自动销毁)
*/
export function setToken(data: DataInfo<Date>) {
export function setToken(data: DataInfo<number>) {
let expires = 0;
const { accessToken, refreshToken } = data;
const { isRemembered, loginDay } = useUserStoreHook();
expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳,将此处代码改为expires = data.expires,然后把上面的DataInfo<Date>改成DataInfo<number>即可
expires = data.expires; // 如果后端直接设置时间戳,将此处代码改为expires = data.expires,然后把上面的DataInfo<Date>改成DataInfo<number>即可
const cookieString = JSON.stringify({ accessToken, expires, refreshToken });
expires > 0
......
......@@ -72,16 +72,12 @@ export function transformRoutes(menuList: MenuItem[]): treeItem[] {
menuList.forEach((item: MenuItem) => {
// 如果有父菜单项,则将其添加到父菜单项的children中
if (item.type === 2 && item.parentId) {
console.log("itemname", item.name, item.parentId);
const parentItem = map.get(item.parentId);
if (parentItem) {
if (!parentItem.children) {
parentItem.children = [];
parentItem.children.push(item);
} else {
parentItem.children.push(item);
}
parentItem.children.push(item);
}
}
if (item.type === 1) {
......@@ -89,8 +85,6 @@ export function transformRoutes(menuList: MenuItem[]): treeItem[] {
tree.push(item);
}
});
// console.log("map", map);
console.log("tree", tree);
// 遍历树,转换每个菜单项为树形结构
const transTree = tree.map((item: MenuItem) => {
......@@ -102,7 +96,7 @@ export function transformRoutes(menuList: MenuItem[]): treeItem[] {
}
return transformData(item);
});
console.log("transTree", transTree);
console.log("tree", tree, transTree);
return transTree;
}
......@@ -13,6 +13,7 @@ import { stringify } from "qs";
import NProgress from "../progress";
import { getToken, formatToken } from "@/utils/auth";
import { useUserStoreHook } from "@/store/modules/user";
import packageJson from "../../../package.json";
// 相关配置请参考:www.axios-js.com/zh-cn/docs/#axios-request-config-1
const defaultConfig: AxiosRequestConfig = {
......@@ -21,7 +22,11 @@ const defaultConfig: AxiosRequestConfig = {
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest"
"X-Requested-With": "XMLHttpRequest",
System: "PureAdmin",
Version: packageJson.version,
"Request-Id": "",
Device: "PC"
},
// 数组格式参数序列化(https://github.com/axios/axios/issues/5142)
paramsSerializer: {
......@@ -52,7 +57,6 @@ class PureHttp {
private static retryOriginalRequest(config: PureHttpRequestConfig) {
return new Promise(resolve => {
PureHttp.requests.push((token: string) => {
debugger;
config.headers["Authorization"] = formatToken(token);
resolve(config);
});
......@@ -80,8 +84,6 @@ class PureHttp {
? config
: new Promise(resolve => {
const data = getToken();
console.log(4444, data);
if (data) {
const now = new Date().getTime();
const expired = parseInt(data.expires) - now <= 0;
......@@ -106,6 +108,9 @@ class PureHttp {
config.headers["Authorization"] = formatToken(
data.accessToken
);
config.headers["Request-Id"] = crypto
.randomUUID()
.replaceAll("-", "");
resolve(config);
}
} else {
......
<script setup lang="ts">
// 引入 vue-i18n 的 useI18n 函数,用于实现国际化支持
import { useI18n } from "vue-i18n";
// 从 Vue 引入 ref 和 reactive 函数,用于创建响应式数据
import { ref, reactive } from "vue";
// 引入自定义动画组件
import Motion from "../utils/motion";
// 引入自定义消息提示工具
import { message } from "@/utils/message";
// 引入手机号登录表单验证规则
import { phoneRules } from "../utils/rule";
// 引入 Element Plus 的 FormInstance 类型,用于表单实例的类型定义
import type { FormInstance } from "element-plus";
// 引入自定义国际化相关函数
import { $t, transformI18n } from "@/plugins/i18n";
// 引入验证码相关的自定义 hook
import { useVerifyCode } from "../utils/verifyCode";
// 引入用户状态管理 hook
import { useUserStoreHook } from "@/store/modules/user";
// 引入图标渲染 hook
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
// 引入手机图标
import Iphone from "~icons/ep/iphone";
// 引入钥匙孔图标
import Keyhole from "~icons/ri/shield-keyhole-line";
// 获取国际化实例
const { t } = useI18n();
// 定义登录加载状态的响应式引用
const loading = ref(false);
// 定义手机号登录表单数据的响应式对象
const ruleForm = reactive({
phone: "",
verifyCode: ""
phone: "", // 手机号,初始为空字符串
verifyCode: "" // 验证码,初始为空字符串
});
// 定义表单实例的响应式引用
const ruleFormRef = ref<FormInstance>();
// 从 useVerifyCode hook 中解构出按钮禁用状态和倒计时文本
const { isDisabled, text } = useVerifyCode();
/**
* 处理手机号登录操作
* @param formEl - 表单实例
*/
const onLogin = async (formEl: FormInstance | undefined) => {
// 设置加载状态为 true,表示正在登录
loading.value = true;
// 如果表单实例不存在,直接返回
if (!formEl) return;
// 对表单进行验证
await formEl.validate(valid => {
if (valid) {
// 模拟登录请求,需根据实际开发进行修改
setTimeout(() => {
// 显示登录成功的消息提示
message(transformI18n($t("login.pureLoginSuccess")), {
type: "success"
});
// 登录完成,设置加载状态为 false
loading.value = false;
}, 2000);
} else {
// 表单验证失败,设置加载状态为 false
loading.value = false;
}
});
};
/**
* 处理返回操作
* 结束验证码倒计时,并将当前页面设置为 0
*/
function onBack() {
// 调用 useVerifyCode 的 end 方法结束验证码倒计时
useVerifyCode().end();
// 调用用户状态管理的方法,将当前页面设置为 0
useUserStoreHook().SET_CURRENTPAGE(0);
}
</script>
<template>
<!-- 手机号登录表单,绑定表单实例、表单数据和验证规则 -->
<el-form ref="ruleFormRef" :model="ruleForm" :rules="phoneRules" size="large">
<!-- 包裹手机号输入项的动画组件,设置默认延迟时间 -->
<Motion>
<!-- 手机号输入项,绑定验证规则中的 phone 属性 -->
<el-form-item prop="phone">
<!-- 手机号输入框,绑定手机号数据,可清空,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.phone"
clearable
......@@ -58,20 +95,26 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 包裹验证码输入项的动画组件,设置延迟时间为 100 毫秒 -->
<Motion :delay="100">
<!-- 验证码输入项,绑定验证规则中的 verifyCode 属性 -->
<el-form-item prop="verifyCode">
<!-- 包含验证码输入框和获取验证码按钮的容器 -->
<div class="w-full flex justify-between">
<!-- 验证码输入框,绑定验证码数据,可清空,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.verifyCode"
clearable
:placeholder="t('login.pureSmsVerifyCode')"
:prefix-icon="useRenderIcon(Keyhole)"
/>
<!-- 获取验证码按钮,根据 isDisabled 状态禁用按钮,点击触发获取验证码操作 -->
<el-button
:disabled="isDisabled"
class="ml-2!"
@click="useVerifyCode().start(ruleFormRef, 'phone')"
>
<!-- 根据倒计时文本显示不同内容 -->
{{
text.length > 0
? text + t("login.pureInfo")
......@@ -82,8 +125,11 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 包裹登录按钮的动画组件,设置延迟时间为 150 毫秒 -->
<Motion :delay="150">
<!-- 登录按钮项 -->
<el-form-item>
<!-- 登录按钮,设置为全宽,主按钮样式,根据 loading 状态显示加载状态,点击触发登录操作 -->
<el-button
class="w-full"
size="default"
......@@ -91,14 +137,19 @@ function onBack() {
:loading="loading"
@click="onLogin(ruleFormRef)"
>
<!-- 显示登录按钮文本 -->
{{ t("login.pureLogin") }}
</el-button>
</el-form-item>
</Motion>
<!-- 包裹返回按钮的动画组件,设置延迟时间为 200 毫秒 -->
<Motion :delay="200">
<!-- 返回按钮项 -->
<el-form-item>
<!-- 返回按钮,设置为全宽,点击触发返回操作 -->
<el-button class="w-full" size="default" @click="onBack">
<!-- 显示返回按钮文本 -->
{{ t("login.pureBack") }}
</el-button>
</el-form-item>
......
<script setup lang="ts">
// 引入 vue-i18n 的 useI18n 函数,用于实现国际化支持
import { useI18n } from "vue-i18n";
// 引入自定义动画组件
import Motion from "../utils/motion";
// 引入自定义二维码组件
import ReQrcode from "@/components/ReQrcode";
// 引入用户状态管理 hook
import { useUserStoreHook } from "@/store/modules/user";
// 获取国际化实例,用于翻译文本
const { t } = useI18n();
</script>
<template>
<!-- 使用 Motion 动画组件包裹二维码,设置顶部和底部外边距为 -2 -->
<Motion class="-mt-2 -mb-2">
<!-- 渲染自定义二维码组件,通过 :text 属性绑定国际化后的二维码内容 -->
<ReQrcode :text="t('login.pureTest')" />
</Motion>
<!-- 使用 Motion 动画组件包裹分割线,设置动画延迟时间为 100 毫秒 -->
<Motion :delay="100">
<!-- 渲染 Element Plus 的分割线组件 -->
<el-divider>
<!-- 在分割线中间显示提示文本,设置文本颜色为灰色,字号为 xs -->
<p class="text-gray-500 text-xs">{{ t("login.pureTip") }}</p>
</el-divider>
</Motion>
<!-- 使用 Motion 动画组件包裹返回按钮,设置动画延迟时间为 150 毫秒 -->
<Motion :delay="150">
<!-- 渲染 Element Plus 的按钮组件,设置按钮宽度为全宽,顶部外边距为 4 -->
<el-button
class="w-full mt-4!"
@click="useUserStoreHook().SET_CURRENTPAGE(0)"
>
<!-- 显示国际化后的返回按钮文本 -->
{{ t("login.pureBack") }}
</el-button>
</Motion>
......
<script setup lang="ts">
// 引入 vue-i18n 的 useI18n 函数,用于实现国际化支持
import { useI18n } from "vue-i18n";
// 从 Vue 引入 ref 和 reactive 函数,用于创建响应式数据
import { ref, reactive } from "vue";
// 引入自定义动画组件
import Motion from "../utils/motion";
// 引入自定义消息提示工具
import { message } from "@/utils/message";
// 引入注册表单验证规则
import { updateRules } from "../utils/rule";
// 引入 Element Plus 的 FormInstance 类型,用于表单实例的类型定义
import type { FormInstance } from "element-plus";
// 引入验证码相关的自定义 hook
import { useVerifyCode } from "../utils/verifyCode";
// 引入自定义国际化相关函数
import { $t, transformI18n } from "@/plugins/i18n";
// 引入用户状态管理 hook
import { useUserStoreHook } from "@/store/modules/user";
// 引入图标渲染 hook
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
// 引入锁图标
import Lock from "~icons/ri/lock-fill";
// 引入手机图标
import Iphone from "~icons/ep/iphone";
// 引入用户图标
import User from "~icons/ri/user-3-fill";
// 引入钥匙孔图标
import Keyhole from "~icons/ri/shield-keyhole-line";
// 获取国际化实例
const { t } = useI18n();
// 定义是否勾选隐私协议的响应式引用,初始值为 false
const checked = ref(false);
// 定义注册加载状态的响应式引用,初始值为 false
const loading = ref(false);
// 定义注册表单数据的响应式对象
const ruleForm = reactive({
username: "",
phone: "",
verifyCode: "",
password: "",
repeatPassword: ""
username: "", // 用户名,初始为空字符串
phone: "", // 手机号,初始为空字符串
verifyCode: "", // 验证码,初始为空字符串
password: "", // 密码,初始为空字符串
repeatPassword: "" // 重复密码,初始为空字符串
});
// 定义表单实例的响应式引用
const ruleFormRef = ref<FormInstance>();
// 从 useVerifyCode hook 中解构出按钮禁用状态和倒计时文本
const { isDisabled, text } = useVerifyCode();
// 定义重复密码的验证规则
const repeatPasswordRule = [
{
/**
* 重复密码验证函数
* @param rule - 当前验证规则
* @param value - 输入的重复密码值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
if (value === "") {
// 重复密码为空时,返回错误信息
callback(new Error(transformI18n($t("login.purePassWordSureReg"))));
} else if (ruleForm.password !== value) {
// 重复密码与密码不一致时,返回错误信息
callback(
new Error(transformI18n($t("login.purePassWordDifferentReg")))
);
} else {
// 重复密码验证通过
callback();
}
},
// 失去焦点时触发验证
trigger: "blur"
}
];
/**
* 处理注册操作
* @param formEl - 表单实例
*/
const onUpdate = async (formEl: FormInstance | undefined) => {
// 设置加载状态为 true,表示正在注册
loading.value = true;
// 如果表单实例不存在,直接返回
if (!formEl) return;
// 对表单进行验证
await formEl.validate(valid => {
if (valid) {
if (checked.value) {
// 模拟请求,需根据实际开发进行修改
// 表单验证通过且勾选了隐私协议,模拟注册请求,需根据实际开发进行修改
setTimeout(() => {
// 显示注册成功的消息提示
message(transformI18n($t("login.pureRegisterSuccess")), {
type: "success"
});
// 注册完成,设置加载状态为 false
loading.value = false;
}, 2000);
} else {
// 未勾选隐私协议,设置加载状态为 false,并显示提示消息
loading.value = false;
message(transformI18n($t("login.pureTickPrivacy")), {
type: "warning"
});
}
} else {
// 表单验证失败,设置加载状态为 false
loading.value = false;
}
});
};
/**
* 处理返回操作
* 结束验证码倒计时,并将当前页面设置为 0
*/
function onBack() {
// 调用 useVerifyCode 的 end 方法结束验证码倒计时
useVerifyCode().end();
// 调用用户状态管理的方法,将当前页面设置为 0
useUserStoreHook().SET_CURRENTPAGE(0);
}
</script>
<template>
<!-- 注册表单,绑定表单实例、表单数据和验证规则,设置表单尺寸为 large -->
<el-form
ref="ruleFormRef"
:model="ruleForm"
:rules="updateRules"
size="large"
>
<!-- 包裹用户名输入项的动画组件,设置默认延迟时间 -->
<Motion>
<!-- 用户名输入项,绑定验证规则 -->
<el-form-item
:rules="[
{
......@@ -92,6 +144,7 @@ function onBack() {
]"
prop="username"
>
<!-- 用户名输入框,绑定用户名数据,可清空,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.username"
clearable
......@@ -101,8 +154,11 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 包裹手机号输入项的动画组件,设置延迟时间为 100 毫秒 -->
<Motion :delay="100">
<!-- 手机号输入项,绑定验证规则中的 phone 属性 -->
<el-form-item prop="phone">
<!-- 手机号输入框,绑定手机号数据,可清空,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.phone"
clearable
......@@ -112,20 +168,26 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 包裹验证码输入项的动画组件,设置延迟时间为 150 毫秒 -->
<Motion :delay="150">
<!-- 验证码输入项,绑定验证规则中的 verifyCode 属性 -->
<el-form-item prop="verifyCode">
<!-- 包含验证码输入框和获取验证码按钮的容器 -->
<div class="w-full flex justify-between">
<!-- 验证码输入框,绑定验证码数据,可清空,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.verifyCode"
clearable
:placeholder="t('login.pureSmsVerifyCode')"
:prefix-icon="useRenderIcon(Keyhole)"
/>
<!-- 获取验证码按钮,根据 isDisabled 状态禁用按钮,点击触发获取验证码操作 -->
<el-button
:disabled="isDisabled"
class="ml-2!"
@click="useVerifyCode().start(ruleFormRef, 'phone')"
>
<!-- 根据倒计时文本显示不同内容 -->
{{
text.length > 0
? text + t("login.pureInfo")
......@@ -136,8 +198,11 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 包裹密码输入项的动画组件,设置延迟时间为 200 毫秒 -->
<Motion :delay="200">
<!-- 密码输入项,绑定验证规则中的 password 属性 -->
<el-form-item prop="password">
<!-- 密码输入框,绑定密码数据,可清空,显示密码可见按钮,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.password"
clearable
......@@ -148,8 +213,11 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 包裹重复密码输入项的动画组件,设置延迟时间为 250 毫秒 -->
<Motion :delay="250">
<!-- 重复密码输入项,绑定重复密码验证规则 -->
<el-form-item :rules="repeatPasswordRule" prop="repeatPassword">
<!-- 重复密码输入框,绑定重复密码数据,可清空,显示密码可见按钮,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.repeatPassword"
clearable
......@@ -160,19 +228,28 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 包裹隐私协议勾选和隐私政策链接的动画组件,设置延迟时间为 300 毫秒 -->
<Motion :delay="300">
<!-- 隐私协议项 -->
<el-form-item>
<!-- 隐私协议勾选框,绑定勾选状态 -->
<el-checkbox v-model="checked">
<!-- 显示隐私协议勾选提示文本 -->
{{ t("login.pureReadAccept") }}
</el-checkbox>
<!-- 隐私政策链接按钮,设置为链接样式,主按钮类型 -->
<el-button link type="primary">
<!-- 显示隐私政策链接文本 -->
{{ t("login.purePrivacyPolicy") }}
</el-button>
</el-form-item>
</Motion>
<!-- 包裹注册按钮的动画组件,设置延迟时间为 350 毫秒 -->
<Motion :delay="350">
<!-- 注册按钮项 -->
<el-form-item>
<!-- 注册按钮,设置为全宽,默认尺寸,主按钮样式,根据 loading 状态显示加载状态,点击触发注册操作 -->
<el-button
class="w-full"
size="default"
......@@ -180,14 +257,19 @@ function onBack() {
:loading="loading"
@click="onUpdate(ruleFormRef)"
>
<!-- 显示注册按钮文本 -->
{{ t("login.pureDefinite") }}
</el-button>
</el-form-item>
</Motion>
<!-- 包裹返回按钮的动画组件,设置延迟时间为 400 毫秒 -->
<Motion :delay="400">
<!-- 返回按钮项 -->
<el-form-item>
<!-- 返回按钮,设置为全宽,默认尺寸,点击触发返回操作 -->
<el-button class="w-full" size="default" @click="onBack">
<!-- 显示返回按钮文本 -->
{{ t("login.pureBack") }}
</el-button>
</el-form-item>
......
<script setup lang="ts">
// 引入 vue-i18n 的 useI18n 函数,用于实现国际化支持
import { useI18n } from "vue-i18n";
// 从 Vue 引入 ref 和 reactive 函数,用于创建响应式数据
import { ref, reactive } from "vue";
// 引入自定义动画组件
import Motion from "../utils/motion";
// 引入自定义消息提示工具
import { message } from "@/utils/message";
// 引入重置密码表单验证规则
import { updateRules } from "../utils/rule";
// 引入 Element Plus 的 FormInstance 类型,用于表单实例的类型定义
import type { FormInstance } from "element-plus";
// 引入验证码相关的自定义 hook
import { useVerifyCode } from "../utils/verifyCode";
// 引入自定义国际化相关函数
import { $t, transformI18n } from "@/plugins/i18n";
// 引入用户状态管理 hook
import { useUserStoreHook } from "@/store/modules/user";
// 引入图标渲染 hook
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
// 引入锁图标
import Lock from "~icons/ri/lock-fill";
// 引入手机图标
import Iphone from "~icons/ep/iphone";
// 引入钥匙孔图标
import Keyhole from "~icons/ri/shield-keyhole-line";
// 获取国际化实例
const { t } = useI18n();
// 定义重置密码操作的加载状态,初始为 false
const loading = ref(false);
// 定义重置密码表单的数据,使用响应式对象
const ruleForm = reactive({
phone: "",
verifyCode: "",
password: "",
repeatPassword: ""
phone: "", // 手机号,初始为空字符串
verifyCode: "", // 验证码,初始为空字符串
password: "", // 新密码,初始为空字符串
repeatPassword: "" // 重复新密码,初始为空字符串
});
// 定义表单实例的引用
const ruleFormRef = ref<FormInstance>();
// 从验证码 hook 中解构出按钮禁用状态和倒计时文本
const { isDisabled, text } = useVerifyCode();
// 定义重复密码的验证规则
const repeatPasswordRule = [
{
/**
* 验证重复密码是否符合要求
* @param rule - 当前验证规则对象
* @param value - 用户输入的重复密码值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
if (value === "") {
// 若重复密码为空,返回错误信息
callback(new Error(transformI18n($t("login.purePassWordSureReg"))));
} else if (ruleForm.password !== value) {
// 若重复密码与新密码不一致,返回错误信息
callback(
new Error(transformI18n($t("login.purePassWordDifferentReg")))
);
} else {
// 验证通过
callback();
}
},
// 触发验证的时机为失去焦点时
trigger: "blur"
}
];
/**
* 处理重置密码操作
* @param formEl - 表单实例
*/
const onUpdate = async (formEl: FormInstance | undefined) => {
// 开始重置操作,设置加载状态为 true
loading.value = true;
// 若表单实例不存在,直接返回
if (!formEl) return;
// 对表单进行验证
await formEl.validate(valid => {
if (valid) {
// 模拟请求,需根据实际开发进行修改
// 模拟请求,实际开发中需替换为真实接口调用
setTimeout(() => {
// 显示密码重置成功的消息
message(transformI18n($t("login.purePassWordUpdateReg")), {
type: "success"
});
// 重置操作完成,设置加载状态为 false
loading.value = false;
}, 2000);
} else {
// 表单验证失败,设置加载状态为 false
loading.value = false;
}
});
};
/**
* 处理返回操作
* 结束验证码倒计时,并将当前页面设置为 0
*/
function onBack() {
// 调用验证码 hook 的 end 方法结束倒计时
useVerifyCode().end();
// 调用用户状态管理的方法,将当前页面设置为 0
useUserStoreHook().SET_CURRENTPAGE(0);
}
</script>
<template>
<!-- 重置密码表单,绑定表单实例、表单数据、验证规则,并设置表单尺寸为 large -->
<el-form
ref="ruleFormRef"
:model="ruleForm"
:rules="updateRules"
size="large"
>
<!-- 手机号输入项,使用动画组件包裹,设置默认延迟 -->
<Motion>
<el-form-item prop="phone">
<!-- 手机号输入框,绑定手机号数据,支持清空,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.phone"
clearable
......@@ -82,20 +131,25 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 验证码输入项,使用动画组件包裹,设置延迟为 100 毫秒 -->
<Motion :delay="100">
<el-form-item prop="verifyCode">
<!-- 包含验证码输入框和获取验证码按钮的容器 -->
<div class="w-full flex justify-between">
<!-- 验证码输入框,绑定验证码数据,支持清空,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.verifyCode"
clearable
:placeholder="t('login.pureSmsVerifyCode')"
:prefix-icon="useRenderIcon(Keyhole)"
/>
<!-- 获取验证码按钮,根据 isDisabled 状态禁用按钮,点击触发获取验证码操作 -->
<el-button
:disabled="isDisabled"
class="ml-2!"
@click="useVerifyCode().start(ruleFormRef, 'phone')"
>
<!-- 根据倒计时文本显示不同内容 -->
{{
text.length > 0
? text + t("login.pureInfo")
......@@ -106,8 +160,10 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 新密码输入项,使用动画组件包裹,设置延迟为 150 毫秒 -->
<Motion :delay="150">
<el-form-item prop="password">
<!-- 新密码输入框,绑定新密码数据,支持清空和显示密码,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.password"
clearable
......@@ -118,8 +174,10 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 重复新密码输入项,使用动画组件包裹,设置延迟为 200 毫秒 -->
<Motion :delay="200">
<el-form-item :rules="repeatPasswordRule" prop="repeatPassword">
<!-- 重复新密码输入框,绑定重复新密码数据,支持清空和显示密码,设置占位提示和前缀图标 -->
<el-input
v-model="ruleForm.repeatPassword"
clearable
......@@ -130,8 +188,10 @@ function onBack() {
</el-form-item>
</Motion>
<!-- 重置密码按钮项,使用动画组件包裹,设置延迟为 250 毫秒 -->
<Motion :delay="250">
<el-form-item>
<!-- 重置密码按钮,设置为全宽,默认尺寸,主按钮样式,根据加载状态显示加载动画,点击触发重置密码操作 -->
<el-button
class="w-full"
size="default"
......@@ -139,14 +199,18 @@ function onBack() {
:loading="loading"
@click="onUpdate(ruleFormRef)"
>
<!-- 显示重置密码按钮文本 -->
{{ t("login.pureDefinite") }}
</el-button>
</el-form-item>
</Motion>
<!-- 返回按钮项,使用动画组件包裹,设置延迟为 300 毫秒 -->
<Motion :delay="300">
<el-form-item>
<!-- 返回按钮,设置为全宽,默认尺寸,点击触发返回操作 -->
<el-button class="w-full" size="default" @click="onBack">
<!-- 显示返回按钮文本 -->
{{ t("login.pureBack") }}
</el-button>
</el-form-item>
......
<script setup lang="ts">
// 引入 vue-i18n 的 useI18n 函数,用于国际化支持
import { useI18n } from "vue-i18n";
// 引入自定义动画组件
import Motion from "./utils/motion";
// 引入 vue-router 的 useRouter 函数,用于路由导航
import { useRouter } from "vue-router";
// 引入自定义消息提示工具
import { message } from "@/utils/message";
// 引入登录表单验证规则
import { loginRules } from "./utils/rule";
// 引入自定义打字动画组件
import TypeIt from "@/components/ReTypeit";
// 引入防抖工具函数
import { debounce } from "@pureadmin/utils";
// 引入导航相关的 hook
import { useNav } from "@/layout/hooks/useNav";
// 引入事件监听工具函数
import { useEventListener } from "@vueuse/core";
// 引入 Element Plus 的表单实例类型
import type { FormInstance } from "element-plus";
// 引入自定义国际化相关函数
import { $t, transformI18n } from "@/plugins/i18n";
// 引入登录操作和第三方登录相关枚举
import { operates, thirdParty } from "./utils/enums";
// 引入布局相关的 hook
import { useLayout } from "@/layout/hooks/useLayout";
// 引入手机号登录组件
import LoginPhone from "./components/LoginPhone.vue";
// 引入注册组件
import LoginRegist from "./components/LoginRegist.vue";
// 引入密码重置组件
import LoginUpdate from "./components/LoginUpdate.vue";
// 引入二维码登录组件
import LoginQrCode from "./components/LoginQrCode.vue";
// 引入加密相关的第三方库
import MD5 from "crypto-js/md5";
// 引入用户状态管理 hook
import { useUserStoreHook } from "@/store/modules/user";
// 引入路由初始化和获取顶部菜单的工具函数
import { initRouter, getTopMenu } from "@/router/utils";
// 引入登录页面所需的静态资源
import { bg, avatar, illustration } from "./utils/static";
// 引入自定义图片验证码组件
import { ReImageVerify } from "@/components/ReImageVerify";
// 引入 Vue 的响应式 API
import { ref, toRaw, reactive, watch, computed } from "vue";
// 引入图标渲染 hook
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
// 引入语言切换相关的 hook
import { useTranslationLang } from "@/layout/hooks/useTranslationLang";
// 引入主题切换相关的 hook
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
// 引入日模式图标
import dayIcon from "@/assets/svg/day.svg?component";
// 引入夜模式图标
import darkIcon from "@/assets/svg/dark.svg?component";
// 引入国际化图标
import globalization from "@/assets/svg/globalization.svg?component";
// 引入锁图标
import Lock from "~icons/ri/lock-fill";
// 引入勾选图标
import Check from "~icons/ep/check";
// 引入用户图标
import User from "~icons/ri/user-3-fill";
// 引入信息图标
import Info from "~icons/ri/information-line";
// 引入钥匙孔图标
import Keyhole from "~icons/ri/shield-keyhole-line";
// 引入 token 操作工具函数
import { setToken, getToken } from "@/utils/auth";
import { any } from "vue-types";
/**
* 定义组件选项
* 设置组件名称为 Login,方便调试和识别
*/
defineOptions({
name: "Login"
});
const radioBox = ref("SMS");
// 定义图片验证码的值
const imgCode = ref("");
// 定义记住登录天数
const loginDay = ref(7);
// 获取路由实例
const router = useRouter();
// 定义登录加载状态
const loading = ref(false);
// 定义是否记住登录状态
const checked = ref(false);
// 定义登录按钮是否禁用状态
const disabled = ref(false);
// 定义表单实例引用
const ruleFormRef = ref<FormInstance>();
// 计算当前登录页面
const currentPage = computed(() => {
return useUserStoreHook().currentPage;
});
// 获取国际化实例
const { t } = useI18n();
// 获取布局相关的初始化方法
const { initStorage } = useLayout();
// 初始化存储
initStorage();
// 获取主题相关的状态和方法
const { dataTheme, overallStyle, dataThemeChange } = useDataThemeChange();
// 进行主题切换
dataThemeChange(overallStyle.value);
// 获取导航相关的状态和方法
const { title, getDropdownItemStyle, getDropdownItemClass } = useNav();
// 获取语言切换相关的状态和方法
const { locale, translationCh, translationEn } = useTranslationLang();
// const ruleForm = reactive({
// username: "admin",
// password: "admin123",
// verifyCode: ""
// });
// 定义登录表单数据
const ruleForm = reactive({
username: "zhangxiaoming",
password: "123456",
verifyCode: ""
});
/**
* 处理登录操作
* @param formEl - 表单实例
*/
const onLogin = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
// 对表单进行验证
console.log("ruleForm", formEl);
await formEl.validate(valid => {
if (valid) {
// 验证通过,设置加载状态
loading.value = true;
useUserStoreHook()
.loginByUsername({
username: ruleForm.username,
password: ruleForm.password
password: MD5(ruleForm.password).toString(),
timer: useUserStoreHook().captchaTime,
captcha: ruleForm.verifyCode
})
.then(res => {
if (res.status === 200 || res.success) {
// 登录成功,设置 token
setToken({
accessToken: res.data.jwt,
refreshToken: "",
expires: new Date(Date.now() + 9999999 * 1000)
accessToken: res.data.token,
refreshToken: res.data.refreshToken,
expires: res.data.expires
// expires: new Date(Date.now() + 9999999 * 1000)
});
console.log("getToken", getToken);
// 获取后端路由
// 初始化路由
return initRouter().then(() => {
disabled.value = true;
// 跳转到顶部菜单对应的路径
router
.push(getTopMenu(true).path)
.then(() => {
// 显示登录成功消息
message(t("login.pureLoginSuccess"), { type: "success" });
})
.finally(() => (disabled.value = false));
});
} else {
// 登录失败,显示失败消息
message(t("login.pureLoginFail"), { type: "error" });
}
})
......@@ -110,12 +171,14 @@ const onLogin = async (formEl: FormInstance | undefined) => {
});
};
// 定义立即执行的防抖函数
const immediateDebounce: any = debounce(
formRef => onLogin(formRef),
1000,
true
);
// 监听键盘按下事件,按下回车键触发登录操作
useEventListener(document, "keydown", ({ code }) => {
if (
["Enter", "NumpadEnter"].includes(code) &&
......@@ -125,22 +188,28 @@ useEventListener(document, "keydown", ({ code }) => {
immediateDebounce(ruleFormRef.value);
});
// 监听图片验证码变化,更新用户状态
watch(imgCode, value => {
useUserStoreHook().SET_VERIFYCODE(value);
});
// 监听是否记住登录状态变化,更新用户状态
watch(checked, bool => {
useUserStoreHook().SET_ISREMEMBERED(bool);
});
// 监听记住登录天数变化,更新用户状态
watch(loginDay, value => {
useUserStoreHook().SET_LOGINDAY(value);
});
</script>
<template>
<!-- 禁止文本选择 -->
<div class="select-none">
<!-- 背景波浪图 -->
<img :src="bg" class="wave" />
<!-- 主题和国际化切换区域 -->
<div class="flex-c absolute right-5 top-3">
<!-- 主题 -->
<!-- 主题切换开关 -->
<el-switch
v-model="dataTheme"
inline-prompt
......@@ -148,13 +217,14 @@ watch(loginDay, value => {
:inactive-icon="darkIcon"
@change="dataThemeChange"
/>
<!-- 国际化 -->
<!-- 国际化切换下拉菜单 -->
<el-dropdown trigger="click">
<globalization
class="hover:text-primary hover:bg-[transparent]! w-[20px] h-[20px] ml-1.5 cursor-pointer outline-hidden duration-300"
/>
<template #dropdown>
<el-dropdown-menu class="translation">
<!-- 简体中文选项 -->
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'zh')"
:class="['dark:text-white!', getDropdownItemClass(locale, 'zh')]"
......@@ -167,6 +237,7 @@ watch(loginDay, value => {
/>
简体中文
</el-dropdown-item>
<!-- 英文选项 -->
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'en')"
:class="['dark:text-white!', getDropdownItemClass(locale, 'en')]"
......@@ -181,13 +252,19 @@ watch(loginDay, value => {
</template>
</el-dropdown>
</div>
<!-- 登录容器 -->
<div class="login-container">
<!-- 插图区域 -->
<div class="img">
<component :is="toRaw(illustration)" />
</div>
<!-- 登录框 -->
<div class="login-box">
<!-- 登录表单 -->
<div class="login-form">
<!-- 用户头像 -->
<avatar class="avatar" />
<!-- 动画包裹的标题 -->
<Motion>
<h2 class="outline-hidden">
<TypeIt
......@@ -196,6 +273,7 @@ watch(loginDay, value => {
</h2>
</Motion>
<!-- 主登录表单,当前页面为 0 时显示 -->
<el-form
v-if="currentPage === 0"
ref="ruleFormRef"
......@@ -203,6 +281,7 @@ watch(loginDay, value => {
:rules="loginRules"
size="large"
>
<!-- 用户名输入项 -->
<Motion :delay="100">
<el-form-item
:rules="[
......@@ -223,6 +302,7 @@ watch(loginDay, value => {
</el-form-item>
</Motion>
<!-- 密码输入项 -->
<Motion :delay="150">
<el-form-item>
<el-input
......@@ -235,7 +315,8 @@ watch(loginDay, value => {
</el-form-item>
</Motion>
<!-- <Motion :delay="200">
<!-- 注释掉的验证码输入项 -->
<Motion :delay="200">
<el-form-item prop="verifyCode">
<el-input
v-model="ruleForm.verifyCode"
......@@ -248,8 +329,9 @@ watch(loginDay, value => {
</template>
</el-input>
</el-form-item>
</Motion> -->
</Motion>
<!-- 记住登录和忘记密码区域 -->
<Motion :delay="250">
<el-form-item>
<div class="w-full h-[20px] flex justify-between items-center">
......@@ -288,14 +370,10 @@ watch(loginDay, value => {
{{ t("login.pureForget") }}
</el-button>
</div>
<el-divider />
<Motion :delay="300">
<el-radio-group v-model="radioBox" size="default">
<el-radio value="SMS" border>SMS</el-radio>
<el-radio value="DBSM" border>DBSM</el-radio>
<el-radio value="ISMS" border>ISMS</el-radio>
</el-radio-group>
</el-form-item>
</Motion>
<!-- 登录按钮 -->
<el-button
class="w-full mt-4!"
size="default"
......@@ -306,10 +384,9 @@ watch(loginDay, value => {
>
{{ t("login.pureLogin") }}
</el-button>
</el-form-item>
</Motion>
<!-- <Motion :delay="300">
<!-- 其他操作按钮 -->
<Motion :delay="300">
<el-form-item>
<div class="w-full h-[20px] flex justify-between items-center">
<el-button
......@@ -323,9 +400,10 @@ watch(loginDay, value => {
</el-button>
</div>
</el-form-item>
</Motion> -->
</Motion>
</el-form>
<!-- 第三方登录分割线和图标 -->
<Motion v-if="currentPage === 0" :delay="350">
<el-form-item>
<el-divider>
......@@ -348,17 +426,18 @@ watch(loginDay, value => {
</div>
</el-form-item>
</Motion>
<!-- 手机号登录 -->
<!-- 手机号登录组件,当前页面为 1 时显示 -->
<LoginPhone v-if="currentPage === 1" />
<!-- 二维码登录 -->
<!-- 二维码登录组件,当前页面为 2 时显示 -->
<LoginQrCode v-if="currentPage === 2" />
<!-- 注册 -->
<!-- 注册组件,当前页面为 3 时显示 -->
<LoginRegist v-if="currentPage === 3" />
<!-- 忘记密码 -->
<!-- 密码重置组件,当前页面为 4 时显示 -->
<LoginUpdate v-if="currentPage === 4" />
</div>
</div>
</div>
<!-- 版权信息 -->
<div
class="w-full flex-c absolute bottom-3 text-sm text-[rgba(0,0,0,0.6)] dark:text-[rgba(220,220,242,0.8)]"
>
......
/**
* 引入国际化函数 $t,用于多语言支持
* 该函数从 @/plugins/i18n 模块导出,可将键值转换为对应语言的文本
*/
import { $t } from "@/plugins/i18n";
/**
* 定义登录操作列表
* 包含手机号登录、二维码登录和注册三个操作,每个操作有对应的国际化标题
*/
const operates = [
{
/** 手机号登录操作的国际化标题 */
title: $t("login.purePhoneLogin")
},
{
/** 二维码登录操作的国际化标题 */
title: $t("login.pureQRCodeLogin")
},
{
/** 注册操作的国际化标题 */
title: $t("login.pureRegister")
}
];
/**
* 定义第三方登录列表
* 包含微信、支付宝、QQ 和微博四种第三方登录方式,每种方式有对应的国际化标题和图标标识
*/
const thirdParty = [
{
/** 微信登录的国际化标题 */
title: $t("login.pureWeChatLogin"),
/** 微信登录的图标标识 */
icon: "wechat"
},
{
/** 支付宝登录的国际化标题 */
title: $t("login.pureAlipayLogin"),
/** 支付宝登录的图标标识 */
icon: "alipay"
},
{
/** QQ 登录的国际化标题 */
title: $t("login.pureQQLogin"),
/** QQ 登录的图标标识 */
icon: "qq"
},
{
/** 微博登录的国际化标题 */
title: $t("login.pureWeiBoLogin"),
/** 微博登录的图标标识 */
icon: "weibo"
}
];
const sys = [
{
title: "SMS"
},
{
title: "DBSM"
},
{
title: "ISMS"
}
];
export { operates, thirdParty, sys };
/**
* 导出登录操作列表和第三方登录列表
* 方便其他模块引入使用这两个常量
*/
export { operates, thirdParty };
import { h, defineComponent, withDirectives, resolveDirective } from "vue";
/** 封装@vueuse/motion动画库中的自定义指令v-motion */
/**
* 封装 @vueuse/motion 动画库中的自定义指令 v-motion
* 该组件用于为包裹的内容添加动画效果,通过 props 可设置动画延迟时间
*/
export default defineComponent({
// 组件名称,方便调试和识别
name: "Motion",
// 定义组件接收的 props
props: {
// 动画延迟时间,类型为数字,默认值为 50 毫秒
delay: {
type: Number,
default: 50
}
},
// 组件的渲染函数
render() {
// 从 props 中解构出 delay 属性
const { delay } = this;
// 解析 v-motion 指令
const motion = resolveDirective("motion");
// 使用 withDirectives 函数为元素添加指令
return withDirectives(
// 创建一个 div 元素
h(
"div",
{},
{
// 默认插槽,渲染子组件或内容
default: () => [this.$slots.default()]
}
),
......@@ -24,10 +36,13 @@ export default defineComponent({
[
motion,
{
// 初始状态,元素透明度为 0,在 y 轴向下偏移 100px
initial: { opacity: 0, y: 100 },
// 进入动画状态,元素透明度变为 1,回到 y 轴初始位置
enter: {
opacity: 1,
y: 0,
// 设置动画过渡效果,包含延迟时间
transition: {
delay
}
......
// 从 Vue 引入 reactive 函数,用于创建响应式对象
import { reactive } from "vue";
// 从 @pureadmin/utils 引入 isPhone 工具函数,用于验证手机号格式
import { isPhone } from "@pureadmin/utils";
// 从 Element Plus 引入 FormRules 类型,用于定义表单验证规则
import type { FormRules } from "element-plus";
// 从 @/plugins/i18n 引入 $t 和 transformI18n 函数,用于国际化文本处理
import { $t, transformI18n } from "@/plugins/i18n";
// 从 @/store/modules/user 引入 useUserStoreHook 函数,用于获取用户状态管理实例
import { useUserStoreHook } from "@/store/modules/user";
/** 6位数字验证码正则 */
export const REGEXP_SIX = /^\d{6}$/;
/** 密码正则(密码格式应为8-18位数字、字母、符号的任意两种组合) */
/** 密码正则(密码格式应为8 - 18位数字、字母、符号的任意两种组合) */
export const REGEXP_PWD =
/^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/;
......@@ -15,31 +20,56 @@ export const REGEXP_PWD =
const loginRules = reactive<FormRules>({
password: [
{
/**
* 密码验证函数
* @param rule - 当前验证规则
* @param value - 输入的密码值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
if (value === "") {
// 密码为空时,返回错误信息
callback(new Error(transformI18n($t("login.purePassWordReg"))));
} else if (!REGEXP_PWD.test(value)) {
// 密码不符合正则规则时,返回错误信息
callback(new Error(transformI18n($t("login.purePassWordRuleReg"))));
} else {
// 密码验证通过
callback();
}
},
// 失去焦点时触发验证
trigger: "blur"
}
],
verifyCode: [
{
/**
* 验证码验证函数
* @param rule - 当前验证规则
* @param value - 输入的验证码值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
console.log("value", value);
console.log(
"useUserStoreHook().verifyCode",
useUserStoreHook().verifyCode
);
if (value === "") {
// 验证码为空时,返回错误信息
callback(new Error(transformI18n($t("login.pureVerifyCodeReg"))));
} else if (useUserStoreHook().verifyCode !== value) {
// 验证码与存储的验证码不一致时,返回错误信息
callback(
new Error(transformI18n($t("login.pureVerifyCodeCorrectReg")))
);
} else {
// 验证码验证通过
callback();
}
},
// 失去焦点时触发验证
trigger: "blur"
}
]
......@@ -49,29 +79,49 @@ const loginRules = reactive<FormRules>({
const phoneRules = reactive<FormRules>({
phone: [
{
/**
* 手机号验证函数
* @param rule - 当前验证规则
* @param value - 输入的手机号值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
if (value === "") {
// 手机号为空时,返回错误信息
callback(new Error(transformI18n($t("login.purePhoneReg"))));
} else if (!isPhone(value)) {
// 手机号格式不正确时,返回错误信息
callback(new Error(transformI18n($t("login.purePhoneCorrectReg"))));
} else {
// 手机号验证通过
callback();
}
},
// 失去焦点时触发验证
trigger: "blur"
}
],
verifyCode: [
{
/**
* 手机登录验证码验证函数
* @param rule - 当前验证规则
* @param value - 输入的验证码值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
if (value === "") {
// 验证码为空时,返回错误信息
callback(new Error(transformI18n($t("login.pureVerifyCodeReg"))));
} else if (!REGEXP_SIX.test(value)) {
// 验证码不是6位数字时,返回错误信息
callback(new Error(transformI18n($t("login.pureVerifyCodeSixReg"))));
} else {
// 验证码验证通过
callback();
}
},
// 失去焦点时触发验证
trigger: "blur"
}
]
......@@ -81,46 +131,80 @@ const phoneRules = reactive<FormRules>({
const updateRules = reactive<FormRules>({
phone: [
{
/**
* 忘记密码时手机号验证函数
* @param rule - 当前验证规则
* @param value - 输入的手机号值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
if (value === "") {
// 手机号为空时,返回错误信息
callback(new Error(transformI18n($t("login.purePhoneReg"))));
} else if (!isPhone(value)) {
// 手机号格式不正确时,返回错误信息
callback(new Error(transformI18n($t("login.purePhoneCorrectReg"))));
} else {
// 手机号验证通过
callback();
}
},
// 失去焦点时触发验证
trigger: "blur"
}
],
verifyCode: [
{
/**
* 忘记密码时验证码验证函数
* @param rule - 当前验证规则
* @param value - 输入的验证码值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
if (value === "") {
// 验证码为空时,返回错误信息
callback(new Error(transformI18n($t("login.pureVerifyCodeReg"))));
} else if (!REGEXP_SIX.test(value)) {
// 验证码不是6位数字时,返回错误信息
callback(new Error(transformI18n($t("login.pureVerifyCodeSixReg"))));
} else {
// 验证码验证通过
callback();
}
},
// 失去焦点时触发验证
trigger: "blur"
}
],
password: [
{
/**
* 忘记密码时新密码验证函数
* @param rule - 当前验证规则
* @param value - 输入的新密码值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
if (value === "") {
// 新密码为空时,返回错误信息
callback(new Error(transformI18n($t("login.purePassWordReg"))));
} else if (!REGEXP_PWD.test(value)) {
// 新密码不符合正则规则时,返回错误信息
callback(new Error(transformI18n($t("login.purePassWordRuleReg"))));
} else {
// 新密码验证通过
callback();
}
},
// 失去焦点时触发验证
trigger: "blur"
}
]
});
/**
* 导出登录、手机登录和忘记密码的表单验证规则
* 方便在登录相关组件中使用这些验证规则
*/
export { loginRules, phoneRules, updateRules };
/**
* 从项目资源目录导入登录页面所需的静态资源
* @description 导入登录背景图、用户头像和插图 SVG 组件,后续会将这些资源导出供其他组件使用
*/
// 导入登录背景图,路径为 @/assets/login/bg.png
import bg from "@/assets/login/bg.png";
// 导入用户头像 SVG 组件,路径为 @/assets/login/avatar.svg,并通过 ?component 标识为组件
import avatar from "@/assets/login/avatar.svg?component";
// 导入登录插图 SVG 组件,路径为 @/assets/login/illustration.svg,并通过 ?component 标识为组件
import illustration from "@/assets/login/illustration.svg?component";
/**
* 导出登录页面所需的静态资源
* @description 将导入的背景图、用户头像和插图 SVG 组件导出,方便其他组件引入使用
*/
export { bg, avatar, illustration };
// 从 element-plus 库中引入 FormInstance 和 FormItemProp 类型,用于处理表单实例和表单项属性
import type { FormInstance, FormItemProp } from "element-plus";
// 从 @pureadmin/utils 工具库中引入 clone 函数,用于深拷贝对象
import { clone } from "@pureadmin/utils";
// 从 Vue 中引入 ref 函数,用于创建响应式引用
import { ref } from "vue";
// 创建一个响应式引用,用于记录按钮是否处于禁用状态,初始值为 false
const isDisabled = ref(false);
// 创建一个响应式引用,用于存储定时器的 ID,初始值为 null
const timer = ref(null);
// 创建一个响应式引用,用于显示倒计时的文本,初始值为空字符串
const text = ref("");
/**
* 自定义 hook,用于处理验证码倒计时逻辑
* @returns {Object} 包含 isDisabled、timer、text、start 和 end 方法的对象
*/
export const useVerifyCode = () => {
/**
* 开始验证码倒计时
* @param {FormInstance | undefined} formEl - 表单实例,用于验证表单字段
* @param {FormItemProp} props - 需要验证的表单项属性
* @param {number} [time=60] - 倒计时的总秒数,默认值为 60 秒
*/
const start = async (
formEl: FormInstance | undefined,
props: FormItemProp,
time = 60
) => {
// 如果表单实例不存在,直接返回
if (!formEl) return;
// 深拷贝初始的倒计时时间,用于后续重置倒计时
const initTime = clone(time, true);
// 异步验证指定的表单字段
await formEl.validateField(props, isValid => {
if (isValid) {
// 验证通过,清除之前可能存在的定时器
clearInterval(timer.value);
// 禁用按钮,防止重复点击
isDisabled.value = true;
// 显示初始的倒计时时间
text.value = `${time}`;
// 设置定时器,每秒更新一次倒计时
timer.value = setInterval(() => {
if (time > 0) {
// 倒计时未结束,时间减 1 并更新显示文本
time -= 1;
text.value = `${time}`;
} else {
// 倒计时结束,清空显示文本
text.value = "";
// 启用按钮
isDisabled.value = false;
// 清除定时器
clearInterval(timer.value);
// 重置倒计时时间
time = initTime;
}
}, 1000);
......@@ -34,6 +62,10 @@ export const useVerifyCode = () => {
});
};
/**
* 结束验证码倒计时
* 清空显示文本,启用按钮,并清除定时器
*/
const end = () => {
text.value = "";
isDisabled.value = false;
......@@ -41,10 +73,15 @@ export const useVerifyCode = () => {
};
return {
/** 按钮是否禁用的响应式引用 */
isDisabled,
/** 存储定时器 ID 的响应式引用 */
timer,
/** 显示倒计时文本的响应式引用 */
text,
/** 开始验证码倒计时的方法 */
start,
/** 结束验证码倒计时的方法 */
end
};
};
<script setup lang="ts">
// 从 vue 导入 ref 函数,用于创建响应式引用
import { ref } from "vue";
// 导入自定义组件 ReCol
import ReCol from "@/components/ReCol";
// 从当前目录的 utils/rule 文件导入表单验证规则
import { formRules } from "./utils/rule";
// 从当前目录的 utils/types 文件导入表单属性类型
import { FormProps } from "./utils/types";
// 从当前目录的 hooks 文件导入公共 Hook
import { usePublicHooks } from "./hooks";
/**
* 定义组件的 props,使用 withDefaults 设置默认值
* @property {FormItemProps} formInline - 表单内联数据,包含部门相关信息
*/
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({
higherDeptOptions: [], // 上级部门选项列表,默认为空数组
parentId: 0, // 上级部门 ID,默认为 0
name: "", // 部门名称,默认为空字符串
principal: "", // 部门负责人,默认为空字符串
phone: "", // 联系电话,默认为空字符串
email: "", // 邮箱地址,默认为空字符串
sort: 0, // 排序值,默认为 0
status: 1, // 部门状态,默认为 1(启用)
remark: "" // 备注信息,默认为空字符串
})
});
// 创建一个 ref 引用,用于获取表单实例
const ruleFormRef = ref();
// 调用公共 Hook 获取 switch 样式
const { switchStyle } = usePublicHooks();
// 创建一个 ref 引用,用于存储表单数据
const newFormInline = ref(props.formInline);
/**
* 获取表单实例的引用
* @returns {unknown} 表单实例的引用
*/
function getRef() {
return ruleFormRef.value;
}
// 暴露 getRef 方法,供父组件调用
defineExpose({ getRef });
</script>
<template>
<!-- 定义一个 Element Plus 表单,绑定表单引用、表单数据和验证规则 -->
<el-form
ref="ruleFormRef"
:model="newFormInline"
:rules="formRules"
label-width="82px"
>
<!-- 定义一个行布局,设置列间距为 30px -->
<el-row :gutter="30">
<!-- 上级部门表单项 -->
<re-col>
<el-form-item label="上级部门">
<!-- 级联选择器,用于选择上级部门 -->
<el-cascader
v-model="newFormInline.parentId"
class="w-full"
:options="newFormInline.higherDeptOptions"
:props="{
value: 'id', // 选项的值字段
label: 'name', // 选项的显示文本字段
emitPath: false, // 不返回选中项的完整路径
checkStrictly: true // 可单独选择任意一级选项
}"
clearable
filterable
placeholder="请选择上级部门"
>
<!-- 自定义选项显示内容 -->
<template #default="{ node, data }">
<span>{{ data.name }}</span>
<!-- 如果不是叶子节点,显示子节点数量 -->
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
</template>
</el-cascader>
</el-form-item>
</re-col>
<!-- 部门名称表单项,添加了验证规则 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="部门名称" prop="name">
<!-- 输入框,用于输入部门名称 -->
<el-input
v-model="newFormInline.name"
clearable
placeholder="请输入部门名称"
/>
</el-form-item>
</re-col>
<!-- 部门负责人表单项 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="部门负责人">
<!-- 输入框,用于输入部门负责人姓名 -->
<el-input
v-model="newFormInline.principal"
clearable
placeholder="请输入部门负责人"
/>
</el-form-item>
</re-col>
<!-- 手机号表单项,添加了验证规则 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="手机号" prop="phone">
<!-- 输入框,用于输入手机号 -->
<el-input
v-model="newFormInline.phone"
clearable
placeholder="请输入手机号"
/>
</el-form-item>
</re-col>
<!-- 邮箱表单项,添加了验证规则 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="邮箱" prop="email">
<!-- 输入框,用于输入邮箱地址 -->
<el-input
v-model="newFormInline.email"
clearable
placeholder="请输入邮箱"
/>
</el-form-item>
</re-col>
<!-- 排序表单项 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="排序">
<!-- 数字输入框,用于输入排序值 -->
<el-input-number
v-model="newFormInline.sort"
class="w-full!"
:min="0"
:max="9999"
controls-position="right"
/>
</el-form-item>
</re-col>
<!-- 部门状态表单项 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="部门状态">
<!-- 开关组件,用于切换部门状态 -->
<el-switch
v-model="newFormInline.status"
inline-prompt
:active-value="1"
:inactive-value="0"
active-text="启用"
inactive-text="停用"
:style="switchStyle"
/>
</el-form-item>
</re-col>
<!-- 备注表单项 -->
<re-col>
<el-form-item label="备注">
<!-- 文本域输入框,用于输入备注信息 -->
<el-input
v-model="newFormInline.remark"
placeholder="请输入备注信息"
type="textarea"
/>
</el-form-item>
</re-col>
</el-row>
</el-form>
</template>
// 抽离可公用的工具函数等用于系统管理页面逻辑
// 从 vue 库导入 computed 函数,用于创建计算属性
import { computed } from "vue";
// 从 @pureadmin/utils 库导入 useDark 函数,用于获取当前是否为深色模式
import { useDark } from "@pureadmin/utils";
/**
* 用于系统管理页面的公共 Hook
* 该 Hook 主要用于处理深色模式相关的样式计算
* @returns 包含深色模式状态、开关样式和标签样式的对象
*/
export function usePublicHooks() {
// 调用 useDark 函数获取当前是否为深色模式的状态
const { isDark } = useDark();
/**
* 计算 `el-switch` 组件的样式
* 根据深色模式状态设置开关开启和关闭时的颜色
*/
const switchStyle = computed(() => {
return {
"--el-switch-on-color": "#6abe39", // 开关开启时的颜色
"--el-switch-off-color": "#e84749" // 开关关闭时的颜色
};
});
/**
* 计算 `el-tag` 组件的样式
* 根据传入的状态和深色模式状态设置标签的文本颜色、背景颜色和边框颜色
*/
const tagStyle = computed(() => {
return (status: number) => {
return status === 1
? {
// 状态为 1 时的标签样式
"--el-tag-text-color": isDark.value ? "#6abe39" : "#389e0d",
"--el-tag-bg-color": isDark.value ? "#172412" : "#f6ffed",
"--el-tag-border-color": isDark.value ? "#274a17" : "#b7eb8f"
}
: {
// 状态不为 1 时的标签样式
"--el-tag-text-color": isDark.value ? "#e84749" : "#cf1322",
"--el-tag-bg-color": isDark.value ? "#2b1316" : "#fff1f0",
"--el-tag-border-color": isDark.value ? "#58191c" : "#ffa39e"
};
};
});
return {
/** 当前网页是否为`dark`模式 */
isDark,
/** 表现更鲜明的`el-switch`组件 */
switchStyle,
/** 表现更鲜明的`el-tag`组件 */
tagStyle
};
}
<template>
<div class="systems">
<h2>Systems</h2>
<slot />
</div>
</template>
<script setup lang="ts">
// 组件逻辑部分
// 从 Vue 导入 ref 函数,用于创建响应式引用
import { ref } from "vue";
// 从当前目录的 utils/hook 文件导入 useDept 自定义 Hook
import { useDept } from "./utils/hook";
// 导入自定义组件 PureTableBar
import { PureTableBar } from "@/components/RePureTableBar";
// 从 ReIcon 组件的 hooks 文件导入 useRenderIcon 函数,用于渲染图标
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
// 导入不同的图标组件
import Delete from "~icons/ep/delete";
import EditPen from "~icons/ep/edit-pen";
import Refresh from "~icons/ep/refresh";
import AddFill from "~icons/ri/add-circle-line";
import { Column } from "element-plus";
const count = ref(0);
/**
* 定义组件选项,设置组件名称为 SystemDept
*/
defineOptions({
name: "SystemDept"
});
const increment = () => {
count.value++;
};
// 创建表单引用和表格引用
const formRef = ref();
const tableRef = ref();
// 调用 useDept Hook,获取部门管理相关的数据和方法
const {
form,
loading,
columns,
dataList,
onSearch,
resetForm,
openDialog,
handleDelete,
handleSelectionChange
} = useDept();
/**
* 处理全屏操作,重置表格高度
*/
function onFullscreen() {
// 调用表格引用的 setAdaptive 方法重置表格高度
tableRef.value.setAdaptive();
}
</script>
<style scoped lang="scss">
.systems {
padding: 20px;
border: 1px solid #ccc;
<template>
<div class="main">
<!-- 搜索表单 -->
<el-form
ref="formRef"
:inline="true"
:model="form"
class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
>
<!-- 部门名称搜索项 -->
<el-form-item label="部门名称:" prop="name">
<el-input
v-model="form.name"
placeholder="请输入部门名称"
clearable
class="w-[180px]!"
/>
</el-form-item>
<!-- 状态搜索项 -->
<el-form-item label="状态:" prop="status">
<el-select
v-model="form.status"
placeholder="请选择状态"
clearable
class="w-[180px]!"
>
<el-option label="启用" :value="1" />
<el-option label="停用" :value="0" />
</el-select>
</el-form-item>
<!-- 搜索和重置按钮项 -->
<el-form-item>
<el-button
type="primary"
:icon="useRenderIcon('ri/search-line')"
:loading="loading"
@click="onSearch"
>
搜索
</el-button>
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
重置
</el-button>
</el-form-item>
</el-form>
<!-- 自定义表格栏组件 -->
<PureTableBar
title="部门管理(仅演示,操作后不生效)"
:columns="columns"
:tableRef="tableRef?.getTableRef()"
@refresh="onSearch"
@fullscreen="onFullscreen"
>
<!-- 自定义按钮插槽 -->
<template #buttons>
<el-button
type="primary"
:icon="useRenderIcon(AddFill)"
@click="openDialog()"
>
新增部门
</el-button>
</template>
<!-- 自定义表格内容插槽 -->
<template v-slot="{ size, dynamicColumns }">
<!-- 自定义表格组件 -->
<pure-table
ref="tableRef"
adaptive
:adaptiveConfig="{ offsetBottom: 45 }"
align-whole="center"
row-key="id"
showOverflowTooltip
table-layout="auto"
default-expand-all
:loading="loading"
:size="size"
:data="dataList"
:columns="dynamicColumns"
:header-cell-style="{
background: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)'
}"
@selection-change="handleSelectionChange"
>
<!-- 自定义操作列插槽 -->
<template #operation="{ row }">
<!-- 修改按钮 -->
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(EditPen)"
@click="openDialog('修改', row)"
>
修改
</el-button>
<!-- 新增子部门按钮 -->
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(AddFill)"
@click="openDialog('新增', { parentId: row.id } as any)"
>
新增
</el-button>
<!-- 删除确认弹窗 -->
<el-popconfirm
:title="`是否确认删除部门名称为${row.name}的这条数据`"
@confirm="handleDelete(row)"
>
<!-- 触发确认弹窗的按钮 -->
<template #reference>
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(Delete)"
>
删除
</el-button>
</template>
</el-popconfirm>
</template>
</pure-table>
</template>
</PureTableBar>
</div>
</template>
<style lang="scss" scoped>
/* 移除表格内滚动条底部的线 */
:deep(.el-table__inner-wrapper::before) {
height: 0;
}
/* 主内容区域样式 */
.main-content {
margin: 24px 24px 0 !important;
}
/* 搜索表单样式 */
.search-form {
:deep(.el-form-item) {
margin-bottom: 12px;
}
}
</style>
import dayjs from "dayjs";
import editForm from "../form.vue";
import { handleTree } from "@/utils/tree";
import { message } from "@/utils/message";
import { getDeptList, addDept, deleteDept, updateDept } from "@/api/systems";
import { usePublicHooks } from "../hooks";
import { addDialog } from "@/components/ReDialog";
import { reactive, ref, onMounted, h } from "vue";
import type { FormItemProps } from "../utils/types";
import { cloneDeep, isAllEmpty, deviceDetection } from "@pureadmin/utils";
// import { cloneDeep, deviceDetection } from "@pureadmin/utils";
/**
* 使用部门管理相关功能的自定义 Hook
* @returns 包含部门管理相关方法和数据的对象
*/
export function useDept() {
// 定义搜索表单的数据
const form = reactive({
name: "",
status: null
});
// 编辑表单的引用
const formRef = ref();
// 存储部门列表数据
const dataList = ref([]);
// 表格加载状态
const loading = ref(true);
// 获取公共 Hook 中的标签样式方法
const { tagStyle } = usePublicHooks();
/**
* 表格列配置类型
* @typedef {Object} TableColumn
* @property {string} label - 列标题
* @property {string} [prop] - 对应数据的字段名
* @property {number} [width] - 列宽度
* @property {number} [minWidth] - 列最小宽度
* @property {string} [align] - 列内容对齐方式
* @property {string} [fixed] - 列固定位置
* @property {string} [slot] - 自定义插槽名称
* @property {Function} [cellRenderer] - 单元格渲染函数
* @property {Function} [formatter] - 数据格式化函数
*/
/**
* 表格列配置列表
* @type {TableColumn[]}
*/
const columns: TableColumnList = [
{
label: "部门名称",
prop: "name",
width: 180,
align: "left" as "center" | "left" | "right"
},
{
label: "排序",
prop: "sort",
minWidth: 70
},
{
label: "状态",
prop: "status",
minWidth: 100,
align: "center" as "center" | "left" | "right",
/**
* 状态列单元格渲染函数
* @param {Object} param - 包含行数据和属性的对象
* @param {Object} param.row - 行数据
* @param {Object} param.props - 列属性
* @returns {JSX.Element} 渲染后的标签元素
*/
cellRenderer: ({ row, props }) => (
<el-tag size={props.size} style={tagStyle.value(row.status)}>
{row.status === 1 ? "启用" : "停用"}
</el-tag>
)
},
{
label: "创建时间",
minWidth: 200,
prop: "createTime",
/**
* 时间格式化函数
* @param {Object} param - 包含创建时间的对象
* @param {string} param.createTime - 创建时间
* @returns {string} 格式化后的时间字符串
*/
formatter: ({ createTime }) =>
dayjs(createTime).format("YYYY-MM-DD HH:mm:ss")
},
// {
// label: "备注",
// prop: "remark",
// minWidth: 320
// },
{
label: "操作",
fixed: "right",
width: 210,
slot: "operation"
}
];
/**
* 处理表格选择变化事件
* @param {Array} val - 选中的行数据数组
*/
function handleSelectionChange(val) {
console.log("handleSelectionChange", val);
}
/**
* 重置搜索表单并重新搜索
* @param {Object} formEl - 表单引用对象
*/
function resetForm(formEl) {
if (!formEl) return;
formEl.resetFields();
onSearch();
}
/**
* 执行搜索操作
*/
async function onSearch() {
// loading.value = true;
// 定义搜索参数
const params = {
pageNum: 1,
pageSize: 200
};
// 获取部门列表数据
const { data } = await getDeptList(params);
// 获取记录数据
let newData = (data as any).records;
if (!isAllEmpty(form.name)) {
// 前端搜索部门名称
newData = newData.filter((item: { name: string }) =>
item.name.includes(form.name)
);
}
if (!isAllEmpty(form.status)) {
// 前端搜索状态
newData = newData.filter(
(item: { status: number | null }) => item.status === form.status
);
}
// 处理数据为树结构
dataList.value = handleTree(newData);
console.log("dataList", dataList.value);
setTimeout(() => {
loading.value = false;
}, 500);
}
/**
* 格式化上级部门选项,根据状态添加禁用属性
* @param {Array} treeList - 部门树列表数据
* @returns {Array} 格式化后的部门树列表数据
*/
function formatHigherDeptOptions(treeList) {
// 根据返回数据的status字段值判断追加是否禁用disabled字段,返回处理后的树结构,用于上级部门级联选择器的展示(实际开发中也是如此,不可能前端需要的每个字段后端都会返回,这时需要前端自行根据后端返回的某些字段做逻辑处理)
if (!treeList || !treeList.length) return;
const newTreeList = [];
for (let i = 0; i < treeList.length; i++) {
treeList[i].disabled = treeList[i].status === 0 ? true : false;
formatHigherDeptOptions(treeList[i].children);
newTreeList.push(treeList[i]);
}
return newTreeList;
}
/**
* 打开新增或编辑部门对话框
* @param {string} [title="新增"] - 对话框标题
* @param {FormItemProps} [row] - 要编辑的部门数据
*/
function openDialog(title = "新增", row?: FormItemProps) {
addDialog({
title: `${title}部门`,
props: {
formInline: {
higherDeptOptions: formatHigherDeptOptions(cloneDeep(dataList.value)),
parentId: row?.parentId ?? 0,
name: row?.name ?? "",
principal: row?.principal ?? "",
phone: row?.phone ?? "",
email: row?.email ?? "",
sort: row?.sort ?? 0,
status: row?.status ?? 1,
remark: row?.remark ?? ""
}
},
width: "40%",
draggable: true,
fullscreen: deviceDetection(),
fullscreenIcon: true,
closeOnClickModal: false,
/**
* 对话框内容渲染函数
* @returns {JSX.Element} 渲染后的编辑表单元素
*/
contentRenderer: () => h(editForm, { ref: formRef, formInline: null }),
/**
* 对话框确认前的回调函数
* @param {Function} done - 完成回调函数,用于关闭对话框
* @param {Object} param - 包含对话框选项的对象
* @param {Object} param.options - 对话框选项
*/
beforeSure: (done, { options }) => {
const FormRef = formRef.value.getRef();
const curData = options.props.formInline as FormItemProps;
/**
* 处理成功后的杂项操作
*/
function chores() {
message(`您${title}了部门名称为${curData.name}的这条数据`, {
type: "success"
});
done(); // 关闭弹框
onSearch(); // 刷新表格数据
}
FormRef.validate(valid => {
if (valid) {
console.log("curData", curData);
// 表单规则校验通过
if (title === "新增") {
addDept(curData).then(res => {
if ((res as any).code === "0") {
chores();
} else {
message((res as any).msg, { type: "error" });
}
});
} else {
if (!row?.id) {
message("id不能为空", { type: "error" });
return;
}
curData.id = row.id; // 修改时需要传入id
updateDept(curData).then(res => {
if ((res as any).code === "0") {
chores();
} else {
message((res as any).msg, { type: "error" });
}
});
}
}
});
}
});
}
/**
* 处理删除部门操作
* @param {Object} row - 要删除的部门数据
* @param {number} row.id - 部门 ID
* @param {string} row.name - 部门名称
*/
function handleDelete(row) {
console.log("handleDelete", row.id);
deleteDept({ id: row.id }).then(res => {
if ((res as any).code === "0") {
message(`您删除了部门名称为${row.name}的这条数据`, { type: "success" });
onSearch();
}
});
}
// 组件挂载后执行搜索操作
onMounted(() => {
onSearch();
});
return {
form,
loading,
columns,
dataList,
/** 搜索 */
onSearch,
/** 重置 */
resetForm,
/** 新增、修改部门 */
openDialog,
/** 删除部门 */
handleDelete,
handleSelectionChange
};
}
// 从 vue 库导入 reactive 函数,用于创建响应式对象
import { reactive } from "vue";
// 从 element-plus 库导入 FormRules 类型,用于定义表单验证规则
import type { FormRules } from "element-plus";
// 从 @pureadmin/utils 库导入 isPhone 和 isEmail 函数,用于验证手机号和邮箱格式
import { isPhone, isEmail } from "@pureadmin/utils";
/**
* 自定义表单规则校验
* 此对象包含了部门表单中各个字段的验证规则,使用 reactive 函数将其转换为响应式对象。
*/
export const formRules = reactive(<FormRules>{
// 部门名称字段的验证规则
name: [
// 验证部门名称是否为必填项,若为空则提示 "部门名称为必填项",在失去焦点时触发验证
{ required: true, message: "部门名称为必填项", trigger: "blur" }
],
// 联系电话字段的验证规则
phone: [
{
/**
* 自定义验证函数,用于验证联系电话格式
* @param rule - 当前验证规则对象
* @param value - 当前字段的值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
// 若联系电话为空,则直接通过验证
if (value === "") {
callback();
}
// 若联系电话格式不正确,则返回错误信息
else if (!isPhone(value)) {
callback(new Error("请输入正确的手机号码格式"));
}
// 若联系电话格式正确,则通过验证
else {
callback();
}
},
// 在失去焦点时触发验证
trigger: "blur"
// 如果想在点击确定按钮时触发这个校验,trigger 设置成 click 即可
// trigger: "click"
}
],
// 邮箱字段的验证规则
email: [
{
/**
* 自定义验证函数,用于验证邮箱格式
* @param rule - 当前验证规则对象
* @param value - 当前字段的值
* @param callback - 验证回调函数,用于返回验证结果
*/
validator: (rule, value, callback) => {
// 若邮箱为空,则直接通过验证
if (value === "") {
callback();
}
// 若邮箱格式不正确,则返回错误信息
else if (!isEmail(value)) {
callback(new Error("请输入正确的邮箱格式"));
}
// 若邮箱格式正确,则通过验证
else {
callback();
}
},
trigger: "blur"
}
]
});
/**
* 部门表单单项属性接口
* 定义了部门表单中每个字段的类型和属性,用于规范部门相关表单数据的结构。
*/
interface FormItemProps {
/**
* 上级部门选项列表
* 每个选项是一个键值对对象,键为字符串类型,值为任意类型。
*/
higherDeptOptions: Record<string, unknown>[];
/**
* 上级部门 ID
* 用于标识当前部门所属的上级部门,为数字类型。
*/
parentId: number;
/**
* 部门名称
* 用于存储部门的具体名称,为字符串类型。
*/
name: string;
/**
* 部门负责人
* 记录部门负责人的姓名,为字符串类型。
*/
principal: string;
/**
* 联系电话
* 可以是字符串或数字类型,用于存储部门的联系电话。
*/
phone: string | number;
/**
* 邮箱地址
* 用于存储部门的联系邮箱,为字符串类型。
*/
email: string;
/**
* 排序值
* 用于对部门进行排序,为数字类型。
*/
sort: number;
/**
* 部门状态
* 用数字表示部门的状态,如启用、停用等。
*/
status: number;
/**
* 备注信息
* 用于存储关于部门的额外说明信息,为字符串类型。
*/
remark: string;
/**
* 部门 ID
* 可选属性,用于唯一标识一个部门,为数字类型。
*/
id?: number;
}
/**
* 部门表单属性接口
* 包含一个部门表单单项属性对象,用于规范整个部门表单的数据结构。
*/
interface FormProps {
/**
* 表单内联数据
* 包含部门表单各项属性的对象,类型为 FormItemProps。
*/
formInline: FormItemProps;
}
/**
* 导出部门表单单项属性和表单属性类型
* 方便在其他文件中引用这些类型,确保数据类型的一致性。
*/
export type { FormItemProps, FormProps };
// 从 Vue 中引入 computed 函数,用于创建计算属性
import { computed } from "vue";
// 从 @pureadmin/utils 包中引入 useDark 函数,用于检测当前是否为暗黑模式
import { useDark } from "@pureadmin/utils";
/**
* 定义一个组合式函数 usePublicHooks,用于获取系统管理页面所需的公共状态和样式
* 该函数封装了暗黑模式检测、el-switch 组件样式和 el-tag 组件样式的逻辑
* @returns 包含 isDark、switchStyle 和 tagStyle 的对象
*/
export function usePublicHooks() {
// 调用 useDark 函数获取当前是否为暗黑模式的状态
const { isDark } = useDark();
/**
* 计算 el-switch 组件的样式
* 根据设计需求,设置开启和关闭状态的颜色
*/
const switchStyle = computed(() => {
return {
// 开启状态的颜色
"--el-switch-on-color": "#6abe39",
// 关闭状态的颜色
"--el-switch-off-color": "#e84749"
};
});
/**
* 计算 el-tag 组件的样式
* 根据传入的 status 参数和当前的暗黑模式状态,动态设置标签的文本颜色、背景颜色和边框颜色
* @param status - 状态值,根据不同的值应用不同的样式
* @returns 包含 CSS 变量的对象,用于设置 el-tag 组件的样式
*/
const tagStyle = computed(() => {
return (status: number) => {
return status === 1
? {
// 状态为 1 时,根据暗黑模式设置标签的文本颜色
"--el-tag-text-color": isDark.value ? "#6abe39" : "#389e0d",
// 状态为 1 时,根据暗黑模式设置标签的背景颜色
"--el-tag-bg-color": isDark.value ? "#172412" : "#f6ffed",
// 状态为 1 时,根据暗黑模式设置标签的边框颜色
"--el-tag-border-color": isDark.value ? "#274a17" : "#b7eb8f"
}
: {
// 状态不为 1 时,根据暗黑模式设置标签的文本颜色
"--el-tag-text-color": isDark.value ? "#e84749" : "#cf1322",
// 状态不为 1 时,根据暗黑模式设置标签的背景颜色
"--el-tag-bg-color": isDark.value ? "#2b1316" : "#fff1f0",
// 状态不为 1 时,根据暗黑模式设置标签的边框颜色
"--el-tag-border-color": isDark.value ? "#58191c" : "#ffa39e"
};
};
});
return {
/** 当前网页是否为`dark`模式 */
isDark,
/** 表现更鲜明的`el-switch`组件 */
switchStyle,
/** 表现更鲜明的`el-tag`组件 */
tagStyle
};
}
<script setup lang="ts">
// 从 Vue 导入 ref 函数,用于创建响应式引用
import { ref } from "vue";
// 导入自定义组件 ReCol
import ReCol from "@/components/ReCol";
// 从当前目录下的 utils/rule 文件导入表单验证规则
import { formRules } from "./utils/rule";
// 从当前目录下的 utils/types 文件导入表单属性类型
import { FormProps } from "./utils/types";
// 从 i18n 插件导入国际化转换函数
import { transformI18n } from "@/plugins/i18n";
// 从自定义组件 ReIcon 导入图标选择器组件
import { IconSelect } from "@/components/ReIcon";
// 导入自定义分段选择器组件
import Segmented from "@/components/ReSegmented";
// 导入自定义动画选择器组件
import ReAnimateSelector from "@/components/ReAnimateSelector";
// 从当前目录下的 utils/enums 文件导入各种选项配置
import {
menuTypeOptions,
showLinkOptions,
fixedTagOptions,
keepAliveOptions,
hiddenTagOptions,
showParentOptions,
frameLoadingOptions
} from "./utils/enums";
/**
* 定义组件的 props,并设置默认值
* @property formInline - 表单数据对象,包含菜单相关信息
*/
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({
type: 1, // 默认菜单类型为 1
higherMenuOptions: [], // 上级菜单选项默认为空数组
parentId: 0, // 父菜单 ID 默认为 0
name: "", // 菜单名称默认为空字符串
path: "", // 菜单路径默认为空字符串
component: "", // 菜单组件路径默认为空字符串
perm: "", // 菜单权限标识默认为空字符串
visible: true, // 菜单默认可见
sort: 0, // 菜单排序默认为 0
icon: "", // 菜单图标默认为空字符串
frameSrc: "" // iframe 链接地址默认为空字符串
// rank: 99,
// redirect: "",
// extraIcon: "",
// enterTransition: "",
// leaveTransition: "",
// activePath: "",
// auths: "",
// frameLoading: true,
// keepAlive: false,
// hiddenTag: false,
// fixedTag: false,
// showLink: true,
// showParent: false
})
});
// 创建一个响应式引用,用于存储表单实例
const ruleFormRef = ref();
// 创建一个响应式引用,用于存储表单数据,初始值为 props 传入的 formInline
const newFormInline = ref(props.formInline);
/**
* 获取表单实例引用
* @returns {Object|null} 表单实例引用,如果不存在则返回 null
*/
function getRef() {
return ruleFormRef.value;
}
// 暴露 getRef 方法,供父组件调用
defineExpose({ getRef });
</script>
<template>
<!-- 定义一个 Element Plus 表单,绑定表单实例引用、表单数据和验证规则 -->
<el-form
ref="ruleFormRef"
:model="newFormInline"
:rules="formRules"
label-width="82px"
>
<!-- 定义一个栅格行,设置列间距为 30 -->
<el-row :gutter="30">
<!-- 菜单类型选择列 -->
<re-col>
<el-form-item label="菜单类型">
<!-- 使用 Element Plus 单选按钮组选择菜单类型 -->
<el-radio-group v-model="newFormInline.type" size="default">
<el-radio-button :label="1">菜单</el-radio-button>
<el-radio-button :label="2">目录</el-radio-button>
<el-radio-button :label="3">外链</el-radio-button>
<el-radio-button :label="4">按钮</el-radio-button>
</el-radio-group>
</el-form-item>
</re-col>
<!-- 上级菜单选择列 -->
<re-col>
<el-form-item label="上级菜单">
<!-- 使用 Element Plus 级联选择器选择上级菜单 -->
<el-cascader
v-model="newFormInline.parentId"
class="w-full"
:options="newFormInline.higherMenuOptions"
:props="{
value: 'id',
label: 'name',
emitPath: false,
checkStrictly: true
}"
clearable
filterable
placeholder="请选择上级菜单"
>
<!-- 自定义级联选择器的选项显示内容 -->
<template #default="{ node, data }">
<span>{{ transformI18n(data.name) }}</span>
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
</template>
</el-cascader>
</el-form-item>
</re-col>
<!-- 菜单名称输入列 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="菜单名称" prop="name">
<!-- 使用 Element Plus 输入框输入菜单名称 -->
<el-input
v-model="newFormInline.name"
clearable
placeholder="请输入菜单名称"
/>
</el-form-item>
</re-col>
<!--
<re-col v-if="newFormInline.type !== 3" :value="12" :xs="24" :sm="24">
<el-form-item label="路由名称" prop="name">
<el-input
v-model="newFormInline.name"
clearable
placeholder="请输入路由名称"
/>
</el-form-item>
</re-col>
-->
<!-- 路由路径输入列,当菜单类型不为外链时显示 -->
<re-col v-if="newFormInline.type !== 3" :value="12" :xs="24" :sm="24">
<el-form-item label="路由路径" prop="path">
<!-- 使用 Element Plus 输入框输入路由路径 -->
<el-input
v-model="newFormInline.path"
clearable
placeholder="请输入路由路径"
/>
</el-form-item>
</re-col>
<!-- 权限标识输入列 -->
<re-col :value="12" :xs="24" :sm="24">
<!-- 按钮级别权限设置 -->
<el-form-item label="权限标识" prop="perm">
<!-- 使用 Element Plus 输入框输入权限标识 -->
<el-input
v-model="newFormInline.perm"
clearable
placeholder="请输入权限标识"
/>
</el-form-item>
</re-col>
<!-- 组件路径输入列,当菜单类型为 0 时显示 -->
<re-col v-show="newFormInline.type === 0" :value="12" :xs="24" :sm="24">
<el-form-item label="组件路径">
<!-- 使用 Element Plus 输入框输入组件路径 -->
<el-input
v-model="newFormInline.component"
clearable
placeholder="请输入组件路径"
/>
</el-form-item>
</re-col>
<!-- 菜单排序输入列 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="菜单排序">
<!-- 使用 Element Plus 数字输入框输入菜单排序 -->
<el-input-number
v-model="newFormInline.sort"
class="w-full!"
:min="1"
:max="9999"
controls-position="right"
/>
</el-form-item>
</re-col>
<!--
<re-col v-show="newFormInline.type === 0" :value="12" :xs="24" :sm="24">
<el-form-item label="路由重定向">
<el-input
v-model="newFormInline.redirect"
clearable
placeholder="请输入默认跳转地址"
/>
</el-form-item>
</re-col>
-->
<!-- 菜单可见性选择列,当菜单类型不为外链时显示 -->
<re-col v-show="newFormInline.type !== 3" :value="12" :xs="24" :sm="24">
<el-form-item label="菜单">
<!-- 使用自定义分段选择器选择菜单可见性 -->
<Segmented
:modelValue="newFormInline.visible === 0 ? true : false"
:options="showLinkOptions"
@change="
({ option: { value } }) => {
newFormInline.visible = value;
}
"
/>
</el-form-item>
</re-col>
<!-- 菜单图标选择列,当菜单类型不为外链时显示 -->
<re-col v-show="newFormInline.type !== 3" :value="12" :xs="24" :sm="24">
<el-form-item label="菜单图标">
<!-- 使用自定义图标选择器选择菜单图标 -->
<IconSelect v-model="newFormInline.icon" class="w-full" />
</el-form-item>
</re-col>
<!--
<re-col v-show="newFormInline.type !== 3" :value="12" :xs="24" :sm="24">
<el-form-item label="右侧图标">
<el-input
v-model="newFormInline.extraIcon"
clearable
placeholder="菜单名称右侧的额外图标"
/>
</el-form-item>
</re-col>
-->
<!--
<re-col v-show="newFormInline.type < 2" :value="12" :xs="24" :sm="24">
<el-form-item label="进场动画">
<ReAnimateSelector
v-model="newFormInline.enterTransition"
placeholder="请选择页面进场加载动画"
/>
</el-form-item>
</re-col>
<re-col v-show="newFormInline.type < 2" :value="12" :xs="24" :sm="24">
<el-form-item label="离场动画">
<ReAnimateSelector
v-model="newFormInline.leaveTransition"
placeholder="请选择页面离场加载动画"
/>
</el-form-item>
</re-col>
-->
<!--
<re-col v-show="newFormInline.type === 0" :value="12" :xs="24" :sm="24">
<el-form-item label="菜单激活">
<el-input
v-model="newFormInline.activePath"
clearable
placeholder="请输入需要激活的菜单"
/>
</el-form-item>
</re-col>
-->
<!-- 链接地址输入列,当菜单类型为外链时显示 -->
<re-col v-show="newFormInline.type === 3" :value="12" :xs="24" :sm="24">
<!-- iframe -->
<el-form-item label="链接地址">
<!-- 使用 Element Plus 输入框输入 iframe 链接地址 -->
<el-input
v-model="newFormInline.frameSrc"
clearable
placeholder="请输入 iframe 链接地址"
/>
</el-form-item>
</re-col>
<!--
<re-col v-if="newFormInline.type === 1" :value="12" :xs="24" :sm="24">
<el-form-item label="加载动画">
<Segmented
:modelValue="newFormInline.frameLoading ? 0 : 1"
:options="frameLoadingOptions"
@change="
({ option: { value } }) => {
newFormInline.frameLoading = value;
}
"
/>
</el-form-item>
</re-col>
-->
<!--
<re-col v-show="newFormInline.type !== 3" :value="12" :xs="24" :sm="24">
<el-form-item label="父级菜单">
<Segmented
:modelValue="newFormInline.showParent ? 0 : 1"
:options="showParentOptions"
@change="
({ option: { value } }) => {
newFormInline.showParent = value;
}
"
/>
</el-form-item>
</re-col>
-->
<!--
<re-col v-show="newFormInline.type < 2" :value="12" :xs="24" :sm="24">
<el-form-item label="缓存页面">
<Segmented
:modelValue="newFormInline.keepAlive ? 0 : 1"
:options="keepAliveOptions"
@change="
({ option: { value } }) => {
newFormInline.keepAlive = value;
}
"
/>
</el-form-item>
</re-col>
-->
<!--
<re-col v-show="newFormInline.type < 2" :value="12" :xs="24" :sm="24">
<el-form-item label="标签页">
<Segmented
:modelValue="newFormInline.hiddenTag ? 1 : 0"
:options="hiddenTagOptions"
@change="
({ option: { value } }) => {
newFormInline.hiddenTag = value;
}
"
/>
</el-form-item>
</re-col>
-->
<!--
<re-col v-show="newFormInline.type < 2" :value="12" :xs="24" :sm="24">
<el-form-item label="固定标签页">
<Segmented
:modelValue="newFormInline.fixedTag ? 0 : 1"
:options="fixedTagOptions"
@change="
({ option: { value } }) => {
newFormInline.fixedTag = value;
}
"
/>
</el-form-item>
</re-col>
-->
</el-row>
</el-form>
</template>
<template>
<div class="systems">
<h2>Systems</h2>
<slot />
</div>
</template>
<script setup lang="ts">
// 组件逻辑部分
// 从 Vue 引入 ref 函数,用于创建响应式引用
import { ref } from "vue";
// 引入菜单相关的自定义 hook
import { useMenu } from "./utils/hook";
// 引入国际化转换函数
import { transformI18n } from "@/plugins/i18n";
// 引入自定义表格栏组件
import { PureTableBar } from "@/components/RePureTableBar";
// 引入图标渲染 hook
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
const count = ref(0);
// 引入删除图标
import Delete from "~icons/ep/delete";
// 引入编辑图标
import EditPen from "~icons/ep/edit-pen";
// 引入刷新图标
import Refresh from "~icons/ep/refresh";
// 引入添加图标
import AddFill from "~icons/ri/add-circle-line";
const increment = () => {
count.value++;
};
/**
* 定义组件选项
* 设置组件名称为 SystemMenu
*/
defineOptions({
name: "SystemMenu"
});
// 创建表单引用
const formRef = ref();
// 创建表格引用
const tableRef = ref();
/**
* 从 useMenu hook 中解构出所需的状态和方法
* form: 搜索表单数据
* loading: 加载状态
* columns: 表格列配置
* dataList: 表格数据列表
* onSearch: 执行搜索操作的方法
* resetForm: 重置表单并重新搜索的方法
* openDialog: 打开新增或修改对话框的方法
* handleDelete: 处理删除操作的方法
* handleSelectionChange: 处理表格选择变化的方法
*/
const {
form,
loading,
columns,
dataList,
onSearch,
resetForm,
openDialog,
handleDelete,
handleSelectionChange
} = useMenu();
/**
* 处理全屏操作
* 重置表格高度
*/
function onFullscreen() {
// 重置表格高度
tableRef.value.setAdaptive();
}
</script>
<style scoped lang="scss">
.systems {
padding: 20px;
border: 1px solid #ccc;
<template>
<div class="main">
<!-- 搜索表单 -->
<el-form
ref="formRef"
:inline="true"
:model="form"
class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
>
<!-- 菜单名称输入项 -->
<el-form-item label="菜单名称:" prop="title">
<el-input
v-model="form.name"
placeholder="请输入菜单名称"
clearable
class="w-[180px]!"
/>
</el-form-item>
<el-form-item>
<!-- 搜索按钮,点击触发搜索操作 -->
<el-button
type="primary"
:icon="useRenderIcon('ri/search-line')"
:loading="loading"
@click="onSearch"
>
搜索
</el-button>
<!-- 重置按钮,点击触发重置表单并重新搜索 -->
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
重置
</el-button>
</el-form-item>
</el-form>
<!-- 自定义表格栏组件 -->
<PureTableBar
title="菜单管理(仅演示,操作后不生效)"
:columns="columns"
:isExpandAll="false"
:tableRef="tableRef?.getTableRef()"
@refresh="onSearch"
@fullscreen="onFullscreen"
>
<!-- 自定义按钮插槽 -->
<template #buttons>
<!-- 新增菜单按钮,点击打开新增对话框 -->
<el-button
type="primary"
:icon="useRenderIcon(AddFill)"
@click="openDialog()"
>
新增菜单
</el-button>
</template>
<!-- 表格插槽 -->
<template v-slot="{ size, dynamicColumns }">
<!-- 自定义表格组件 -->
<pure-table
ref="tableRef"
adaptive
:adaptiveConfig="{ offsetBottom: 45 }"
align-whole="center"
row-key="id"
showOverflowTooltip
table-layout="auto"
:loading="loading"
:size="size"
:data="dataList"
:columns="dynamicColumns"
:header-cell-style="{
background: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)'
}"
@selection-change="handleSelectionChange"
>
<!-- 操作列插槽 -->
<template #operation="{ row }">
<!-- 修改按钮,点击打开修改对话框 -->
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(EditPen)"
@click="openDialog('修改', row)"
>
修改
</el-button>
<!-- 新增子菜单按钮,仅当菜单类型不为 3 时显示 -->
<el-button
v-show="row.menuType !== 3"
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(AddFill)"
@click="openDialog('新增', { parentId: row.id } as any)"
>
新增
</el-button>
<!-- 删除确认弹窗 -->
<el-popconfirm
:title="`是否确认删除菜单名称为${transformI18n(row.title)}的这条数据${row?.children?.length > 0 ? '。注意下级菜单也会一并删除,请谨慎操作' : ''}`"
@confirm="handleDelete(row)"
>
<template #reference>
<!-- 删除按钮 -->
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(Delete)"
>
删除
</el-button>
</template>
</el-popconfirm>
</template>
</pure-table>
</template>
</PureTableBar>
</div>
</template>
<style lang="scss" scoped>
/* 隐藏表格内部包装器的底部边框 */
:deep(.el-table__inner-wrapper::before) {
height: 0;
}
/* 主内容区域样式 */
.main-content {
margin: 24px 24px 0 !important;
}
/* 搜索表单样式 */
.search-form {
:deep(.el-form-item) {
margin-bottom: 12px;
}
}
</style>
// 从 @/components/ReSegmented 导入 OptionsType 类型,用于规范选项数据的结构
import type { OptionsType } from "@/components/ReSegmented";
/**
* 菜单类型选项列表
* 定义了不同菜单类型的选项,每个选项包含标签和对应的值
*/
const menuTypeOptions: Array<OptionsType> = [
{
label: "菜单", // 菜单类型的显示标签
value: 1 // 菜单类型对应的值
},
{
label: "目录", // 目录类型的显示标签
value: 2 // 目录类型对应的值
},
{
label: "外链", // 外链类型的显示标签
value: 3 // 外链类型对应的值
},
{
label: "按钮", // 按钮类型的显示标签
value: 4 // 按钮类型对应的值
}
];
/**
* 显示链接选项列表
* 定义了菜单是否显示链接的选项,每个选项包含标签、提示信息和对应的值
*/
const showLinkOptions: Array<OptionsType> = [
{
label: "显示", // 显示链接的显示标签
tip: "会在菜单中显示", // 显示链接的提示信息
value: true // 显示链接对应的值
},
{
label: "隐藏", // 隐藏链接的显示标签
tip: "不会在菜单中显示", // 隐藏链接的提示信息
value: false // 隐藏链接对应的值
}
];
/**
* 固定标签选项列表
* 定义了菜单标签是否固定显示的选项,每个选项包含标签、提示信息和对应的值
*/
const fixedTagOptions: Array<OptionsType> = [
{
label: "固定", // 固定标签的显示标签
tip: "当前菜单名称固定显示在标签页且不可关闭", // 固定标签的提示信息
value: true // 固定标签对应的值
},
{
label: "不固定", // 不固定标签的显示标签
tip: "当前菜单名称不固定显示在标签页且可关闭", // 不固定标签的提示信息
value: false // 不固定标签对应的值
}
];
/**
* 缓存选项列表
* 定义了菜单页面是否缓存的选项,每个选项包含标签、提示信息和对应的值
*/
const keepAliveOptions: Array<OptionsType> = [
{
label: "缓存", // 缓存的显示标签
tip: "会保存该页面的整体状态,刷新后会清空状态", // 缓存的提示信息
value: true // 缓存对应的值
},
{
label: "不缓存", // 不缓存的显示标签
tip: "不会保存该页面的整体状态", // 不缓存的提示信息
value: false // 不缓存对应的值
}
];
/**
* 隐藏标签选项列表
* 定义了菜单名称或自定义信息是否允许添加到标签页的选项,每个选项包含标签、提示信息和对应的值
*/
const hiddenTagOptions: Array<OptionsType> = [
{
label: "允许", // 允许添加到标签页的显示标签
tip: "当前菜单名称或自定义信息允许添加到标签页", // 允许添加到标签页的提示信息
value: false // 允许添加到标签页对应的值
},
{
label: "禁止", // 禁止添加到标签页的显示标签
tip: "当前菜单名称或自定义信息禁止添加到标签页", // 禁止添加到标签页的提示信息
value: true // 禁止添加到标签页对应的值
}
];
/**
* 显示父级菜单选项列表
* 定义了是否显示父级菜单的选项,每个选项包含标签、提示信息和对应的值
*/
const showParentOptions: Array<OptionsType> = [
{
label: "显示", // 显示父级菜单的显示标签
tip: "会显示父级菜单", // 显示父级菜单的提示信息
value: true // 显示父级菜单对应的值
},
{
label: "隐藏", // 隐藏父级菜单的显示标签
tip: "不会显示父级菜单", // 隐藏父级菜单的提示信息
value: false // 隐藏父级菜单对应的值
}
];
/**
* 框架加载动画选项列表
* 定义了是否开启首次加载动画的选项,每个选项包含标签、提示信息和对应的值
*/
const frameLoadingOptions: Array<OptionsType> = [
{
label: "开启", // 开启加载动画的显示标签
tip: "有首次加载动画", // 开启加载动画的提示信息
value: true // 开启加载动画对应的值
},
{
label: "关闭", // 关闭加载动画的显示标签
tip: "无首次加载动画", // 关闭加载动画的提示信息
value: false // 关闭加载动画对应的值
}
];
/**
* 导出所有选项列表
* 方便在其他文件中引用这些选项数据
*/
export {
menuTypeOptions,
showLinkOptions,
fixedTagOptions,
keepAliveOptions,
hiddenTagOptions,
showParentOptions,
frameLoadingOptions
};
import editForm from "../form.vue";
import { handleTree } from "@/utils/tree";
import { message } from "@/utils/message";
import { getMenuList, addMenu, updateMenu, deleteMenu } from "@/api/systems";
import { transformI18n } from "@/plugins/i18n";
import { addDialog } from "@/components/ReDialog";
import { reactive, ref, onMounted, h } from "vue";
import type { FormItemProps } from "../utils/types";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { cloneDeep, isAllEmpty, deviceDetection } from "@pureadmin/utils";
// import { c } from "node_modules/vite/dist/node/moduleRunnerTransport.d-CXw_Ws6P";
/**
* 自定义 Hook,用于管理菜单相关的状态和操作
* @returns 包含菜单相关状态和操作的对象
*/
export function useMenu() {
// 定义表单数据,使用 reactive 创建响应式对象,初始包含菜单名称字段
const form = reactive({
name: ""
});
// 定义表单引用,用于获取表单实例
const formRef = ref();
// 定义菜单数据列表,使用 ref 创建响应式对象,初始为空数组
const dataList = ref([]);
// 定义加载状态,使用 ref 创建响应式对象,初始为 true 表示正在加载
const loading = ref(true);
/**
* 根据菜单类型返回对应的标签类型或文本描述
* @param type - 菜单类型,取值为 1, 2, 3, 4
* @param text - 是否返回文本描述,默认为 false
* @returns 标签类型或文本描述
*/
const getMenuType = (type, text = false) => {
switch (type) {
case 1:
return text ? "菜单" : "primary";
case 2:
return text ? "目录" : "warning";
case 3:
return text ? "外链" : "danger";
case 4:
return text ? "按钮" : "info";
}
};
// 定义表格列配置,用于渲染菜单表格
const columns = [
{
label: "菜单名称",
prop: "name",
align: "left",
// 自定义单元格渲染函数,显示菜单图标和名称
cellRenderer: ({ row }) => (
<>
<span class="inline-block mr-1">
{h(useRenderIcon(row.icon), {
style: { paddingTop: "1px" }
})}
</span>
<span>{transformI18n(row.name)}</span>
</>
)
},
{
label: "菜单类型",
prop: "type",
width: 100,
// 自定义单元格渲染函数,显示菜单类型标签
cellRenderer: ({ row, props }) => (
<el-tag size={props.size} type={getMenuType(row.type)} effect="plain">
{getMenuType(row.type, true)}
</el-tag>
)
},
{
label: "路由路径",
prop: "path"
},
{
label: "组件路径",
prop: "component",
// 自定义格式化函数,若组件路径为空则显示路由路径
formatter: ({ path, component }) =>
isAllEmpty(component) ? path : component
},
{
label: "权限标识",
prop: "perm"
},
{
label: "排序",
prop: "sort",
width: 100
},
{
label: "隐藏",
prop: "visible",
// 自定义格式化函数,将布尔值转换为中文描述
formatter: ({ visible }) => (visible ? "是" : "否"),
width: 100
},
{
label: "操作",
fixed: "right",
width: 210,
slot: "operation"
}
];
/**
* 处理表格选中项变化的事件
* @param val - 选中项的值
*/
function handleSelectionChange(val) {
console.log("handleSelectionChange", val);
}
/**
* 重置表单数据并重新搜索菜单
* @param formEl - 表单实例
*/
function resetForm(formEl) {
if (!formEl) return;
// 手动清空菜单名称
form.name = "";
// 重置表单字段
formEl.resetFields();
// 重新搜索菜单
onSearch();
}
/**
* 搜索菜单数据
*/
async function onSearch() {
// 设置加载状态为正在加载
loading.value = true;
// 请求菜单列表数据
const { data } = await getMenuList({ name: "", pageNum: 1, pageSize: 100 }); // 这里是返回一维数组结构,前端自行处理成树结构,返回格式要求:唯一id加父节点parentId,parentId取父节点id
// 获取返回数据中的记录列表
let newData = (data as any).records;
if (!isAllEmpty(form.name)) {
// 前端搜索菜单名称,过滤出包含搜索关键词的菜单
newData = newData.filter(item =>
transformI18n(item.name).includes(form.name)
);
}
// 将一维数组数据处理成树结构
dataList.value = handleTree(newData);
// 模拟加载延迟,500ms 后设置加载状态为完成
setTimeout(() => {
loading.value = false;
}, 500);
}
/**
* 格式化上级菜单选项,将菜单数据转换为可用于下拉选择的选项
* @param treeList - 菜单树状数据
* @returns 格式化后的菜单选项数组
*/
function formatHigherMenuOptions(treeList) {
if (!treeList || !treeList.length) return;
const newTreeList = [];
for (let i = 0; i < treeList.length; i++) {
// 对菜单名称进行国际化处理
treeList[i].name = transformI18n(treeList[i].name);
// 递归处理子菜单
formatHigherMenuOptions(treeList[i].children);
newTreeList.push(treeList[i]);
}
return newTreeList;
}
/**
* 打开新增或编辑菜单的对话框
* @param title - 对话框标题,默认为 "新增"
* @param row - 要编辑的菜单数据,可选
*/
function openDialog(title = "新增", row?: FormItemProps) {
addDialog({
title: `${title}菜单`,
props: {
formInline: {
type: row?.type ?? 1,
higherMenuOptions: formatHigherMenuOptions(cloneDeep(dataList.value)),
parentId: row?.parentId ?? 0,
name: row?.name ?? "",
path: row?.path ?? "",
component: row?.component ?? "",
perm: row?.perm ?? "",
visible: row?.visible ?? true,
sort: row?.sort ?? 99,
icon: row?.icon ?? "",
frameSrc: row?.frameSrc ?? ""
// title: row?.title ?? "",
// rank: row?.rank ?? 99,
// redirect: row?.redirect ?? "",
// extraIcon: row?.extraIcon ?? "",
// enterTransition: row?.enterTransition ?? "",
// leaveTransition: row?.leaveTransition ?? "",
// activePath: row?.activePath ?? "",
// auths: row?.auths ?? "",
// frameLoading: row?.frameLoading ?? true,
// keepAlive: row?.keepAlive ?? false,
// hiddenTag: row?.hiddenTag ?? false,
// fixedTag: row?.fixedTag ?? false,
// showLink: row?.showLink ?? true,
// showParent: row?.showParent ?? false
}
},
width: "45%",
draggable: true,
fullscreen: deviceDetection(),
fullscreenIcon: true,
closeOnClickModal: false,
// 对话框内容渲染函数,渲染编辑表单
contentRenderer: () => h(editForm, { ref: formRef, formInline: null }),
// 对话框确认前的回调函数
beforeSure: (done, { options }) => {
// 获取表单实例
const FormRef = formRef.value.getRef();
// 获取表单数据
const curData = options.props.formInline as FormItemProps;
/**
* 处理成功操作后的通用任务,如提示消息、关闭对话框、刷新表格数据
*/
function chores() {
message(
`您${title}了菜单名称为${transformI18n(curData.name)}的这条数据`,
{
type: "success"
}
);
// 关闭对话框
done();
// 刷新表格数据
onSearch();
}
// 验证表单
FormRef.validate(valid => {
if (valid) {
// 表单规则校验通过
if (title === "新增") {
curData.visible =
curData.visible || curData.visible === 0 ? 1 : 0;
// 调用新增菜单接口
addMenu(curData).then(res => {
if ((res as any).code === "0") {
chores();
}
});
} else {
if (!row?.id) {
message("id不能为空", { type: "error" });
return;
}
curData.id = row?.id;
curData.visible =
curData.visible || curData.visible === 0 ? 1 : 0;
// 调用更新菜单接口
updateMenu(curData).then(res => {
if ((res as any).code === "0") {
chores();
}
});
}
}
});
}
});
}
/**
* 处理删除菜单的操作
* @param row - 要删除的菜单数据
*/
function handleDelete(row) {
if (!row.id) {
message("id不能为空", { type: "error" });
return;
}
// 调用删除菜单接口
deleteMenu({ id: row.id }).then(res => {
if ((res as any).code === "0") {
message(`您删除了菜单名称为${transformI18n(row.title)}的这条数据`, {
type: "success"
});
// 刷新表格数据
onSearch();
} else {
message((res as any).msg, { type: "error" });
}
});
}
// 组件挂载后执行搜索操作
onMounted(() => {
onSearch();
});
return {
form,
loading,
columns,
dataList,
/** 搜索 */
onSearch,
/** 重置 */
resetForm,
/** 新增、修改菜单 */
openDialog,
/** 删除菜单 */
handleDelete,
handleSelectionChange
};
}
/**
* 从 Vue 导入 reactive 函数,用于创建响应式对象
*/
import { reactive } from "vue";
/**
* 从 element-plus 导入 FormRules 类型,用于定义表单验证规则
*/
import type { FormRules } from "element-plus";
/**
* 自定义表单规则校验
* 该常量使用 reactive 函数创建一个响应式的表单验证规则对象,
* 包含对菜单名称、路由路径和权限标识的必填项验证。
*/
export const formRules = reactive(<FormRules>{
// 菜单名称的验证规则,要求该项为必填项,失去焦点时触发验证
name: [{ required: true, message: "菜单名称为必填项", trigger: "blur" }],
// 路由路径的验证规则,要求该项为必填项,失去焦点时触发验证
path: [{ required: true, message: "路由路径为必填项", trigger: "blur" }],
// 权限标识的验证规则,要求该项为必填项,失去焦点时触发验证
perm: [{ required: true, message: "权限标识为必填项", trigger: "blur" }]
});
/**
* 定义表单项的属性接口
*/
interface FormItemProps {
/** 菜单类型(0代表菜单、1代表iframe、2代表外链、3代表按钮)*/
type: number;
/** 上级菜单选项数组,每个选项是一个键值对对象 */
higherMenuOptions: Record<string, unknown>[];
/** 父菜单的ID */
parentId: number;
/** 菜单名称 */
name: string;
/** 菜单路径 */
path: string;
/** 菜单对应的组件 */
component: string;
/** 菜单权限 */
perm: string;
/** 菜单是否可见,值可以是数字或布尔类型 */
visible: number | boolean;
/** 菜单排序 */
sort: number;
/** 菜单图标 */
icon: string;
/** iframe 菜单的源地址 */
frameSrc: string;
/** 菜单ID,可选属性 */
id?: any;
// /** 菜单标题 */
// title: string;
// /** 菜单层级 */
// rank: number;
// /** 菜单重定向路径 */
// redirect: string;
// /** 额外图标 */
// extraIcon: string;
// /** 进入过渡动画 */
// enterTransition: string;
// /** 离开过渡动画 */
// leaveTransition: string;
// /** 激活路径 */
// activePath: string;
// /** 菜单权限列表 */
// auths: string;
// /** iframe 菜单加载状态 */
// frameLoading: boolean;
// /** 是否缓存组件 */
// keepAlive: boolean;
// /** 是否隐藏标签页 */
// hiddenTag: boolean;
// /** 是否固定标签页 */
// fixedTag: boolean;
// /** 是否显示链接 */
// showLink: boolean;
// /** 是否显示父菜单 */
// showParent: boolean;
}
/**
* 定义表单属性接口
*/
interface FormProps {
/** 内联表单,其属性遵循 FormItemProps 接口 */
formInline: FormItemProps;
}
/**
* 导出 FormItemProps 和 FormProps 类型,供其他模块使用
*/
export type { FormItemProps, FormProps };
<script setup lang="ts">
// 从 Vue 引入 ref 函数,用于创建响应式引用
import { ref } from "vue";
// 引入当前目录下 utils 文件夹里 rule.ts 文件定义的表单验证规则
import { formRules } from "./utils/rule";
// 引入当前目录下 utils 文件夹里 types.ts 文件定义的表单属性类型
import { FormProps } from "./utils/types";
/**
* 定义组件的 props
* 使用 withDefaults 函数为 props 设置默认值,确保在父组件未传递 props 时,组件仍能正常工作。
* @property formInline - 表单内联数据,包含角色名称、角色标识和备注信息,默认初始值为空字符串。
*/
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({
name: "",
code: "",
remark: ""
})
});
// 创建一个响应式引用,用于获取表单实例
const ruleFormRef = ref();
// 创建一个响应式引用,存储表单内联数据,初始值来自 props 传入的数据
const newFormInline = ref(props.formInline);
/**
* 获取表单实例
* @returns 返回表单实例,如果实例存在则返回,不存在则返回 undefined。
*/
function getRef() {
return ruleFormRef.value;
}
// 将 getRef 方法暴露给父组件,使得父组件可以调用该方法获取表单实例
defineExpose({ getRef });
</script>
<template>
<!-- 定义一个 Element Plus 的表单组件 -->
<el-form
ref="ruleFormRef"
:model="newFormInline"
:rules="formRules"
label-width="82px"
>
<!-- 角色名称表单项 -->
<el-form-item label="角色名称" prop="name">
<!-- 输入框组件,绑定角色名称数据,支持清空输入内容,有占位提示 -->
<el-input
v-model="newFormInline.name"
clearable
placeholder="请输入角色名称"
/>
</el-form-item>
<!-- 角色标识表单项 -->
<el-form-item label="角色标识" prop="code">
<!-- 输入框组件,绑定角色标识数据,支持清空输入内容,有占位提示 -->
<el-input
v-model="newFormInline.code"
clearable
placeholder="请输入角色标识"
/>
</el-form-item>
<!-- 备注表单项 -->
<el-form-item label="备注">
<!-- 文本域输入框组件,绑定备注信息数据,有占位提示 -->
<el-input
v-model="newFormInline.remark"
placeholder="请输入备注信息"
type="textarea"
/>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
// 引入当前目录下 utils 文件夹里的 useRole hook,用于处理角色管理相关逻辑
import { useRole } from "./utils/hook";
// 从 Vue 引入响应式 API 和生命周期钩子
import { ref, computed, nextTick, onMounted } from "vue";
// 引入自定义表格栏组件
import { PureTableBar } from "@/components/RePureTableBar";
// 引入图标渲染 hook
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
// 引入工具函数
import {
delay,
subBefore,
deviceDetection,
useResizeObserver
} from "@pureadmin/utils";
// 引入图标
// import Database from "~icons/ri/database-2-line";
// import More from "~icons/ep/more-filled";
import Delete from "~icons/ep/delete";
import EditPen from "~icons/ep/edit-pen";
import Refresh from "~icons/ep/refresh";
import Menu from "~icons/ep/menu";
import AddFill from "~icons/ri/add-circle-line";
import Close from "~icons/ep/close";
import Check from "~icons/ep/check";
/**
* 定义组件选项
* 设置组件名称为 SystemRole,方便调试和组件识别
*/
defineOptions({
name: "SystemRole"
});
/**
* 计算图标类名
* 返回一个包含多个 CSS 类名的数组,用于设置图标的样式
*/
const iconClass = computed(() => {
return [
"w-[22px]",
"h-[22px]",
"flex",
"justify-center",
"items-center",
"outline-hidden",
"rounded-[4px]",
"cursor-pointer",
"transition-colors",
"hover:bg-[#0000000f]",
"dark:hover:bg-[#ffffff1f]",
"dark:hover:text-[#ffffffd9]"
];
});
// 创建树形组件引用
const treeRef = ref();
// 创建表单引用
const formRef = ref();
// 创建表格引用
const tableRef = ref();
// 创建内容区域引用
const contentRef = ref();
// 创建树形组件高度的响应式变量
const treeHeight = ref();
/**
* 从 useRole hook 中解构出所需的状态和方法
* form: 搜索表单数据
* isShow: 是否显示菜单权限弹窗
* curRow: 当前选中的角色行数据
* loading: 表格加载状态
* columns: 表格列配置
* rowStyle: 表格行样式方法
* dataList: 表格数据列表
* treeData: 树形菜单数据
* treeProps: 树形组件属性配置
* isLinkage: 是否联动操作
* pagination: 分页配置
* isExpandAll: 是否展开所有树形节点
* isSelectAll: 是否全选所有树形节点
* treeSearchValue: 树形搜索框的值
* onSearch: 执行搜索操作的方法
* resetForm: 重置表单并重新搜索的方法
* openDialog: 打开新增或修改对话框的方法
* handleMenu: 处理菜单权限操作的方法
* handleSave: 保存菜单权限的方法
* handleDelete: 处理删除角色操作的方法
* filterMethod: 树形组件过滤方法
* transformI18n: 国际化转换函数
* onQueryChanged: 处理搜索框内容变化的方法
* handleSizeChange: 处理每页显示数量变化的方法
* handleCurrentChange: 处理当前页码变化的方法
* handleSelectionChange: 处理表格选择变化的方法
*/
const {
form,
isShow,
curRow,
loading,
columns,
rowStyle,
dataList,
treeData,
treeProps,
isLinkage,
pagination,
isExpandAll,
isSelectAll,
treeSearchValue,
// buttonClass,
onSearch,
resetForm,
openDialog,
handleMenu,
handleSave,
handleDelete,
filterMethod,
transformI18n,
onQueryChanged,
// handleDatabase,
handleSizeChange,
handleCurrentChange,
handleSelectionChange
} = useRole(treeRef);
/**
* 组件挂载后执行的操作
* 监听内容区域的大小变化,动态设置树形组件的高度
*/
onMounted(() => {
useResizeObserver(contentRef, async () => {
// 等待下一个 DOM 更新周期
await nextTick();
// 延迟 60 毫秒后执行
delay(60).then(() => {
// 获取表格包装器的高度,并转换为数字赋值给树形组件高度变量
treeHeight.value = parseFloat(
subBefore(tableRef.value.getTableDoms().tableWrapper.style.height, "px")
);
});
});
});
</script>
<template>
<div class="systems">
<h2>Systems</h2>
<slot />
<div class="main">
<!-- 搜索表单 -->
<el-form
ref="formRef"
:inline="true"
:model="form"
class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
>
<!-- 角色名称输入项 -->
<el-form-item label="角色名称:" prop="name">
<el-input
v-model="form.name"
placeholder="请输入角色名称"
clearable
class="w-[180px]!"
/>
</el-form-item>
<!-- 角色标识输入项 -->
<el-form-item label="角色标识:" prop="code">
<el-input
v-model="form.code"
placeholder="请输入角色标识"
clearable
class="w-[180px]!"
/>
</el-form-item>
<!-- 状态选择项 -->
<el-form-item label="状态:" prop="status">
<el-select
v-model="form.status"
placeholder="请选择状态"
clearable
class="w-[180px]!"
>
<el-option label="已启用" value="1" />
<el-option label="已停用" value="0" />
</el-select>
</el-form-item>
<!-- 操作按钮项 -->
<el-form-item>
<!-- 搜索按钮,点击触发搜索操作 -->
<el-button
type="primary"
:icon="useRenderIcon('ri/search-line')"
:loading="loading"
@click="onSearch"
>
搜索
</el-button>
<!-- 重置按钮,点击触发重置表单并重新搜索 -->
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
重置
</el-button>
</el-form-item>
</el-form>
<!-- 内容区域 -->
<div
ref="contentRef"
:class="['flex', deviceDetection() ? 'flex-wrap' : '']"
>
<!-- 自定义表格栏组件 -->
<PureTableBar
:class="[isShow && !deviceDetection() ? 'w-[60vw]!' : 'w-full']"
style="transition: width 220ms cubic-bezier(0.4, 0, 0.2, 1)"
title="角色管理(仅演示,操作后不生效)"
:columns="columns"
@refresh="onSearch"
>
<!-- 自定义按钮插槽 -->
<template #buttons>
<!-- 新增角色按钮,点击打开新增对话框 -->
<el-button
type="primary"
:icon="useRenderIcon(AddFill)"
@click="openDialog()"
>
新增角色
</el-button>
</template>
<!-- 表格插槽 -->
<template v-slot="{ size, dynamicColumns }">
<!-- 自定义表格组件 -->
<pure-table
ref="tableRef"
align-whole="center"
showOverflowTooltip
table-layout="auto"
:loading="loading"
:size="size"
adaptive
:row-style="rowStyle"
:adaptiveConfig="{ offsetBottom: 108 }"
:data="dataList"
:columns="dynamicColumns"
:pagination="{ ...pagination, size }"
:header-cell-style="{
background: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)'
}"
@selection-change="handleSelectionChange"
@page-size-change="handleSizeChange"
@page-current-change="handleCurrentChange"
>
<!-- 操作列插槽 -->
<template #operation="{ row }">
<!-- 修改按钮,点击打开修改对话框 -->
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(EditPen)"
@click="openDialog('修改', row)"
>
修改
</el-button>
<!-- 删除确认弹窗 -->
<el-popconfirm
:title="`是否确认删除角色名称为${row.name}的这条数据`"
@confirm="handleDelete(row)"
>
<template #reference>
<!-- 删除按钮 -->
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(Delete)"
>
删除
</el-button>
</template>
</el-popconfirm>
<!-- 权限按钮,点击处理菜单权限操作 -->
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(Menu)"
@click="handleMenu(row)"
>
权限
</el-button>
<!-- 注释掉的下拉菜单,可用于扩展功能 -->
<!-- <el-dropdown>
<el-button
class="ml-3 mt-[2px]"
link
type="primary"
:size="size"
:icon="useRenderIcon(More)"
/>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-button
:class="buttonClass"
link
type="primary"
:size="size"
:icon="useRenderIcon(Menu)"
@click="handleMenu"
>
菜单权限
</el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button
:class="buttonClass"
link
type="primary"
:size="size"
:icon="useRenderIcon(Database)"
@click="handleDatabase"
>
数据权限
</el-button>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown> -->
</template>
</pure-table>
</template>
</PureTableBar>
<!-- 菜单权限弹窗,当 isShow 为 true 时显示 -->
<div
v-if="isShow"
class="min-w-[calc(100vw-60vw-268px)]! w-full mt-2 px-2 pb-2 bg-bg_color ml-2 overflow-auto"
>
<!-- 弹窗头部 -->
<div class="flex justify-between w-full px-3 pt-5 pb-4">
<div class="flex">
<!-- 关闭图标,点击隐藏菜单权限弹窗 -->
<span :class="iconClass">
<IconifyIconOffline
v-tippy="{
content: '关闭'
}"
class="dark:text-white"
width="18px"
height="18px"
:icon="Close"
@click="handleMenu"
/>
</span>
<!-- 保存图标,点击保存菜单权限 -->
<span :class="[iconClass, 'ml-2']">
<IconifyIconOffline
v-tippy="{
content: '保存菜单权限'
}"
class="dark:text-white"
width="18px"
height="18px"
:icon="Check"
@click="handleSave"
/>
</span>
</div>
<!-- 弹窗标题 -->
<p class="font-bold truncate">
菜单权限
{{ `${curRow?.name ? `(${curRow.name})` : ""}` }}
</p>
</div>
<!-- 树形搜索框 -->
<el-input
v-model="treeSearchValue"
placeholder="请输入菜单进行搜索"
class="mb-1"
clearable
@input="onQueryChanged"
/>
<!-- 操作复选框 -->
<div class="flex flex-wrap">
<el-checkbox v-model="isExpandAll" label="展开/折叠" />
<el-checkbox v-model="isSelectAll" label="全选/全不选" />
<el-checkbox v-model="isLinkage" label="父子联动" />
</div>
<!-- 树形组件 -->
<el-tree-v2
ref="treeRef"
show-checkbox
:data="treeData"
:props="treeProps"
:height="treeHeight"
:check-strictly="!isLinkage"
:filter-method="filterMethod"
>
<!-- 树形节点默认插槽 -->
<template #default="{ node }">
<!-- 显示树形节点标签,进行国际化转换 -->
<span>{{ transformI18n(node.label) }}</span>
</template>
</el-tree-v2>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// 组件逻辑部分
import { ref } from "vue";
const count = ref(0);
<style lang="scss" scoped>
/* 设置下拉菜单中图标元素的外边距为 0 */
:deep(.el-dropdown-menu__item i) {
margin: 0;
}
const increment = () => {
count.value++;
};
</script>
/* 设置主内容区域的外边距 */
.main-content {
margin: 24px 24px 0 !important;
}
<style scoped lang="scss">
.systems {
padding: 20px;
border: 1px solid #ccc;
/* 设置搜索表单内表单项的底部外边距 */
.search-form {
:deep(.el-form-item) {
margin-bottom: 12px;
}
}
</style>
import dayjs from "dayjs";
import editForm from "../form.vue";
import { handleTree } from "@/utils/tree";
import { message } from "@/utils/message";
import { ElMessageBox } from "element-plus";
import { usePublicHooks } from "../../hooks";
import { transformI18n } from "@/plugins/i18n";
import { addDialog } from "@/components/ReDialog";
import type { FormItemProps } from "../utils/types";
import type { PaginationProps } from "@pureadmin/table";
import { deviceDetection } from "@pureadmin/utils";
import {
getRoleList,
addRole,
updateRole,
deleteRole,
getMenuListByRoleId,
getMenuList,
bindMenuByRoleId
} from "@/api/systems";
import { type Ref, reactive, ref, onMounted, h, toRaw, watch } from "vue";
export function useRole(treeRef: Ref) {
const form = reactive({
name: "",
code: "",
status: "",
pageNum: 1,
pageSize: 10
});
const curRow = ref();
const formRef = ref();
const dataList = ref([]);
const treeIds = ref([]);
const treeData = ref([]);
const isShow = ref(false);
const loading = ref(true);
const isLinkage = ref(false);
const treeSearchValue = ref();
const switchLoadMap = ref({});
const isExpandAll = ref(false);
const isSelectAll = ref(false);
const { switchStyle } = usePublicHooks();
const treeProps = {
value: "id",
label: "name",
children: "children"
};
const pagination = reactive<PaginationProps>({
total: 0,
pageSize: 10,
currentPage: 1,
background: true
});
const columns: TableColumnList = [
{
label: "角色编号",
prop: "id"
},
{
label: "角色名称",
prop: "name"
},
{
label: "角色标识",
prop: "code"
},
{
label: "状态",
cellRenderer: scope => (
<el-switch
size={scope.props.size === "small" ? "small" : "default"}
loading={switchLoadMap.value[scope.index]?.loading}
v-model={scope.row.status}
active-value={1}
inactive-value={0}
active-text="已启用"
inactive-text="已停用"
inline-prompt
style={switchStyle.value}
onChange={() => onChange(scope as any)}
/>
),
minWidth: 90
},
{
label: "备注",
prop: "remark",
minWidth: 160
},
{
label: "创建时间",
prop: "createTime",
minWidth: 160,
formatter: ({ createTime }) =>
dayjs(createTime).format("YYYY-MM-DD HH:mm:ss")
},
{
label: "操作",
fixed: "right",
width: 210,
slot: "operation"
}
];
// const buttonClass = computed(() => {
// return [
// "h-[20px]!",
// "reset-margin",
// "text-gray-500!",
// "dark:text-white!",
// "dark:hover:text-primary!"
// ];
// });
function onChange({ row }) {
ElMessageBox.confirm(
`确认要<strong>${
row.status === 0 ? "停用" : "启用"
}</strong><strong style='color:var(--el-color-primary)'>${
row.name
}</strong>吗?`,
"系统提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
dangerouslyUseHTMLString: true,
draggable: true
}
)
.then(() => {
// 调用修改接口,修改角色状态
const params = {
id: row.id,
status: row.status
};
// 实际开发先调用修改接口,再进行下面操作
updateRole(params).then(res => {
if (res.code === "0") {
message(
`角色名称为${row.name}${row.status === 0 ? "停用" : "启用"}成功`,
{
type: "success"
}
);
}
});
// switchLoadMap.value[index] = Object.assign(
// {},
// switchLoadMap.value[index],
// {
// loading: true
// }
// );
// setTimeout(() => {
// switchLoadMap.value[index] = Object.assign(
// {},
// switchLoadMap.value[index],
// {
// loading: false
// }
// );
// message(`已${row.status === 0 ? "停用" : "启用"}${row.name}`, {
// type: "success"
// });
// }, 300);
})
.catch(() => {
row.status === 0 ? (row.status = 1) : (row.status = 0);
});
}
function handleDelete(row) {
// 删除角色
ElMessageBox.confirm(
`确认要删除角色名称为<strong style='color:var(--el-color-primary)'>${
row.name
}</strong>吗?`,
"系统提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
dangerouslyUseHTMLString: true,
draggable: true
}
)
.then(() => {
deleteRole(row.id).then(res => {
if (res.code === "0") {
message(`角色名称为${row.name}的删除成功`, { type: "success" });
onSearch();
}
});
})
.catch(() => {});
onSearch();
}
function handleSizeChange(val: number) {
console.log(`${val} items per page`);
}
function handleCurrentChange(val: number) {
console.log(`current page: ${val}`);
}
function handleSelectionChange(val) {
console.log("handleSelectionChange", val);
}
async function onSearch() {
loading.value = true;
const { data } = await getRoleList(toRaw(form));
dataList.value = data.records;
pagination.total = data.total;
pagination.pageSize = data.pageSize;
pagination.currentPage = data.currentPage;
setTimeout(() => {
loading.value = false;
}, 500);
}
const resetForm = formEl => {
if (!formEl) return;
formEl.resetFields();
onSearch();
};
function openDialog(title = "新增", row?: FormItemProps) {
addDialog({
title: `${title}角色`,
props: {
formInline: {
name: row?.name ?? "",
code: row?.code ?? "",
remark: row?.remark ?? ""
}
},
width: "40%",
draggable: true,
fullscreen: deviceDetection(),
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => h(editForm, { ref: formRef, formInline: null }),
beforeSure: (done, { options }) => {
const FormRef = formRef.value.getRef();
const curData = options.props.formInline as FormItemProps;
function chores() {
message(`您${title}了角色名称为${curData.name}的这条数据`, {
type: "success"
});
done(); // 关闭弹框
onSearch(); // 刷新表格数据
}
FormRef.validate(valid => {
if (valid) {
console.log("curData", curData);
// 表单规则校验通过
if (title === "新增") {
addRole(curData).then(res => {
if (res.code === "0") {
message(`角色名称为${curData.name}的新增成功`, {
type: "success"
});
// 实际开发先调用新增接口,再进行下面操作
chores();
}
});
// 实际开发先调用新增接口,再进行下面操作
// chores();
} else {
// 实际开发先调用修改接口,再进行下面操作
// chores();
// 把id加到参数里
curData.id = row.id;
updateRole(curData).then(res => {
if (res.code === "0") {
// message(`角色名称为${curData.name}的新增成功`, {
// type: "success"
// });
// 实际开发先调用新增接口,再进行下面操作
chores();
}
});
}
}
});
}
});
}
/** 菜单权限 */
async function handleMenu(row?: any) {
const { id } = row;
if (id) {
curRow.value = row;
isShow.value = true;
const { data } = await getMenuListByRoleId(id);
treeRef.value.setCheckedKeys(data?.map(item => item.id) ?? []);
console.log("treeRef.value", treeRef.value, data);
} else {
curRow.value = null;
isShow.value = false;
}
}
/** 高亮当前权限选中行 */
function rowStyle({ row: { id } }) {
return {
cursor: "pointer",
background: id === curRow.value?.id ? "var(--el-fill-color-light)" : ""
};
}
/** 菜单权限-保存 */
function handleSave() {
const { id, name } = curRow.value;
// 根据用户 id 调用实际项目中菜单权限修改接口
// 添加给角色绑定菜单接口
const params = {
roleId: id,
menuIds: treeRef.value.getCheckedKeys()
};
console.log("params", params);
bindMenuByRoleId(params).then(res => {
if (res.code === "0") {
message(`角色名称为${name}的菜单权限修改成功`, { type: "success" });
}
});
}
/** 数据权限 可自行开发 */
// function handleDatabase() {}
const onQueryChanged = (query: string) => {
treeRef.value!.filter(query);
};
const filterMethod = (query: string, node) => {
return transformI18n(node.title)!.includes(query);
};
onMounted(async () => {
onSearch();
const { data } = await getMenuList({ pageNum: 1, pageSize: 1000 });
// treeIds.value = getKeyList(data, "id");
treeData.value = handleTree(data.records);
});
watch(isExpandAll, val => {
val
? treeRef.value.setExpandedKeys(treeIds.value)
: treeRef.value.setExpandedKeys([]);
});
watch(isSelectAll, val => {
val
? treeRef.value.setCheckedKeys(treeIds.value)
: treeRef.value.setCheckedKeys([]);
});
return {
form,
isShow,
curRow,
loading,
columns,
rowStyle,
dataList,
treeData,
treeProps,
isLinkage,
pagination,
isExpandAll,
isSelectAll,
treeSearchValue,
// buttonClass,
onSearch,
resetForm,
openDialog,
handleMenu,
handleSave,
handleDelete,
filterMethod,
transformI18n,
onQueryChanged,
// handleDatabase,
handleSizeChange,
handleCurrentChange,
handleSelectionChange
};
}
// 从 Vue 中引入 reactive 函数,用于创建响应式对象
import { reactive } from "vue";
// 从 Element Plus 引入 FormRules 类型,用于定义表单验证规则
import type { FormRules } from "element-plus";
/**
* 自定义表单规则校验
* 此常量包含了角色表单中各个字段的验证规则,使用 reactive 函数使其变为响应式对象,
* 方便在表单验证过程中动态更新规则。
*/
export const formRules = reactive(<FormRules>{
// 角色名称字段的验证规则
name: [{ required: true, message: "角色名称为必填项", trigger: "blur" }],
// 角色标识字段的验证规则
code: [{ required: true, message: "角色标识为必填项", trigger: "blur" }]
});
/**
* 定义表单单项属性的接口
* 该接口描述了角色表单中单个表单项的属性结构
*/
interface FormItemProps {
/** 角色名称,必填字段,字符串类型 */
name: string;
/** 角色编号,必填字段,字符串类型 */
code: string;
/** 备注信息,可选字段,字符串类型 */
remark: string;
/** 角色 ID,可选字段,字符串类型 */
id?: string;
}
/**
* 定义表单属性的接口
* 该接口描述了整个角色表单的属性结构,包含一个 FormItemProps 类型的表单内联对象
*/
interface FormProps {
/** 表单内联对象,包含角色表单的各项属性 */
formInline: FormItemProps;
}
/**
* 导出定义的类型
* 导出 FormItemProps 和 FormProps 接口,方便在其他文件中使用这些类型定义
*/
export type { FormItemProps, FormProps };
<script setup lang="ts">
// 从 Vue 引入 ref 函数,用于创建响应式引用
import { ref } from "vue";
// 引入自定义列组件
import ReCol from "@/components/ReCol";
// 引入表单验证规则
import { formRules } from "../utils/rule";
// 引入表单属性类型定义
import { FormProps } from "../utils/types";
// 引入公共 hook
import { usePublicHooks } from "../../hooks";
/**
* 定义组件的 props
* 使用 withDefaults 函数为 props 设置默认值,确保在父组件未传递 props 时,组件能正常初始化。
* @property formInline - 表单内联数据,包含用户相关信息,如昵称、用户名等。
*/
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({
title: "新增",
higherDeptOptions: [],
parentId: 0,
nickname: "",
username: "",
password: "",
mobile: "",
email: "",
gender: "",
status: 1,
remark: "",
deptId: 0
})
});
/**
* 性别选项数组
* 包含男性和女性两个选项,每个选项有对应的值和显示标签。
*/
const sexOptions = [
{
value: 0,
label: "男"
},
{
value: 1,
label: "女"
}
];
// 创建一个响应式引用,用于获取表单实例
const ruleFormRef = ref();
// 从公共 hook 中获取开关样式
const { switchStyle } = usePublicHooks();
// 创建一个响应式引用,存储表单内联数据,初始值来自 props 传入的数据
const newFormInline = ref(props.formInline);
/**
* 获取表单实例
* @returns 返回表单实例,如果实例存在则返回,不存在则返回 undefined。
*/
function getRef() {
return ruleFormRef.value;
}
// 将 getRef 方法暴露给父组件,使得父组件可以调用该方法获取表单实例
defineExpose({ getRef });
</script>
<template>
<!-- 定义一个 Element Plus 的表单组件 -->
<el-form
ref="ruleFormRef"
:model="newFormInline"
:rules="formRules"
label-width="82px"
>
<!-- 定义一个行布局,设置列间距为 30px -->
<el-row :gutter="30">
<!-- 用户昵称表单项 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="用户昵称" prop="nickname">
<!-- 输入框组件,绑定用户昵称数据,支持清空输入内容,有占位提示 -->
<el-input
v-model="newFormInline.nickname"
clearable
placeholder="请输入用户昵称"
/>
</el-form-item>
</re-col>
<!-- 用户名称表单项 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="用户名称" prop="username">
<!-- 输入框组件,绑定用户名称数据,支持清空输入内容,有占位提示 -->
<el-input
v-model="newFormInline.username"
clearable
placeholder="请输入用户名称"
/>
</el-form-item>
</re-col>
<!-- 用户密码表单项,仅在表单标题为“新增”时显示 -->
<!--
<re-col
v-if="newFormInline.title === '新增'"
:value="12"
:xs="24"
:sm="24"
>
<el-form-item label="用户密码" prop="password">
<el-input
v-model="newFormInline.password"
clearable
placeholder="请输入用户密码"
/>
</el-form-item>
</re-col>
-->
<!-- 手机号表单项 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="手机号" prop="mobile">
<!-- 输入框组件,绑定手机号数据,支持清空输入内容,有占位提示 -->
<el-input
v-model="newFormInline.mobile"
clearable
placeholder="请输入手机号"
/>
</el-form-item>
</re-col>
<!-- 邮箱表单项 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="邮箱" prop="email">
<!-- 输入框组件,绑定邮箱数据,支持清空输入内容,有占位提示 -->
<el-input
v-model="newFormInline.email"
clearable
placeholder="请输入邮箱"
/>
</el-form-item>
</re-col>
<!-- 用户性别表单项 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="用户性别">
<!-- 下拉选择框组件,绑定用户性别数据,支持清空选择,有占位提示 -->
<el-select
v-model="newFormInline.gender"
placeholder="请选择用户性别"
class="w-full"
clearable
>
<!-- 循环渲染性别选项 -->
<el-option
v-for="(item, index) in sexOptions"
:key="index"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</re-col>
<!-- 归属部门表单项 -->
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="归属部门">
<!-- 级联选择器组件,绑定归属部门父 ID 数据,支持清空选择、过滤选项,有占位提示 -->
<el-cascader
v-model="newFormInline.parentId"
class="w-full"
:options="newFormInline.higherDeptOptions"
:props="{
value: 'id',
label: 'name',
emitPath: false,
checkStrictly: true
}"
clearable
filterable
placeholder="请选择归属部门"
>
<!-- 自定义级联选择器节点显示内容 -->
<template #default="{ node, data }">
<span>{{ data.name }}</span>
<!-- 非叶子节点显示子节点数量 -->
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
</template>
</el-cascader>
</el-form-item>
</re-col>
<!-- 用户状态表单项,仅在表单标题为“新增”时显示 -->
<!--
<re-col
v-if="newFormInline.title === '新增'"
:value="12"
:xs="24"
:sm="24"
>
<el-form-item label="用户状态">
<el-switch
v-model="newFormInline.status"
inline-prompt
:active-value="1"
:inactive-value="0"
active-text="启用"
inactive-text="停用"
:style="switchStyle"
/>
</el-form-item>
</re-col>
-->
<!-- 备注表单项 -->
<re-col>
<el-form-item label="备注">
<!-- 文本域输入框组件,绑定备注信息数据,有占位提示 -->
<el-input
v-model="newFormInline.remark"
placeholder="请输入备注信息"
type="textarea"
/>
</el-form-item>
</re-col>
</el-row>
</el-form>
</template>
<script setup lang="ts">
// 从 Vue 引入 ref 函数,用于创建响应式引用
import { ref } from "vue";
// 引入自定义列组件
import ReCol from "@/components/ReCol";
// 引入角色表单属性类型定义
import { RoleFormProps } from "../utils/types";
/**
* 定义组件的 props
* 使用 withDefaults 函数为 props 设置默认值,确保在父组件未传递 props 时,组件能正常初始化。
* @property formInline - 表单内联数据,包含用户名、昵称、角色选项和所选角色 ID 列表。
*/
const props = withDefaults(defineProps<RoleFormProps>(), {
formInline: () => ({
username: "",
nickname: "",
roleOptions: [],
ids: []
})
});
// 创建一个响应式引用,存储表单内联数据,初始值来自 props 传入的数据
const newFormInline = ref(props.formInline);
</script>
<template>
<!-- 定义一个 Element Plus 的表单组件,绑定表单数据 -->
<el-form :model="newFormInline">
<!-- 定义一个行布局,设置列间距为 30px -->
<el-row :gutter="30">
<!-- 注释掉的用户名称表单项 -->
<!-- <re-col>
<el-form-item label="用户名称" prop="username">
<el-input disabled v-model="newFormInline.username" />
</el-form-item>
</re-col> -->
<!-- 用户昵称表单项 -->
<re-col>
<el-form-item label="用户昵称" prop="nickname">
<!-- 输入框组件,绑定用户昵称数据,设置为禁用状态 -->
<el-input v-model="newFormInline.nickname" disabled />
</el-form-item>
</re-col>
<!-- 角色列表表单项 -->
<re-col>
<el-form-item label="角色列表" prop="ids">
<!-- 下拉选择框组件,支持多选,绑定所选角色 ID 列表,有占位提示,支持清空选择 -->
<el-select
v-model="newFormInline.ids"
placeholder="请选择"
class="w-full"
clearable
multiple
>
<!-- 循环渲染角色选项 -->
<el-option
v-for="(item, index) in newFormInline.roleOptions"
:key="index"
:value="item.id"
:label="item.name"
>
<!-- 显示角色名称 -->
{{ item.name }}
</el-option>
</el-select>
</el-form-item>
</re-col>
</el-row>
</el-form>
</template>
<script setup lang="ts">
// 从 Vue 引入 ref 函数,用于创建响应式引用
import { ref } from "vue";
// 引入部门树组件
import tree from "./tree.vue";
// 引入用户管理相关的自定义 hook
import { useUser } from "./utils/hook";
// 引入自定义表格栏组件
import { PureTableBar } from "@/components/RePureTableBar";
// 引入图标渲染 hook
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
// 引入上传图标
import Upload from "~icons/ri/upload-line";
// 引入角色图标
import Role from "~icons/ri/admin-line";
// 引入密码图标
import Password from "~icons/ri/lock-password-line";
// 引入更多操作图标
import More from "~icons/ep/more-filled";
// 引入删除图标
import Delete from "~icons/ep/delete";
// 引入编辑图标
import EditPen from "~icons/ep/edit-pen";
// 引入刷新图标
import Refresh from "~icons/ep/refresh";
// 引入添加图标
import AddFill from "~icons/ri/add-circle-line";
/**
* 定义组件选项
* 设置组件名称为 SystemUser,方便调试和识别
*/
defineOptions({
name: "SystemUser"
});
// 创建部门树组件的引用
const treeRef = ref();
// 创建表单的引用
const formRef = ref();
// 创建表格的引用
const tableRef = ref();
/**
* 从 useUser hook 中解构出所需的状态和方法
* form: 搜索表单数据
* loading: 表格加载状态
* columns: 表格列配置
* dataList: 表格数据列表
* treeData: 部门树数据
* treeLoading: 部门树加载状态
* selectedNum: 表格中选中的记录数量
* pagination: 分页配置
* buttonClass: 按钮样式类
* deviceDetection: 设备检测方法
* onSearch: 执行搜索操作的方法
* resetForm: 重置表单并重新搜索的方法
* onbatchDel: 批量删除操作的方法
* openDialog: 打开新增或编辑用户对话框的方法
* onTreeSelect: 部门树节点选择的处理方法
* handleUpdate: 更新用户信息的方法
* handleDelete: 删除单个用户的方法
* handleUpload: 上传用户头像的方法
* handleReset: 重置用户密码的方法
* handleRole: 分配用户角色的方法
* handleSizeChange: 处理每页显示数量变化的方法
* onSelectionCancel: 取消表格选中的方法
* handleCurrentChange: 处理当前页码变化的方法
* handleSelectionChange: 处理表格选择变化的方法
*/
const {
form,
loading,
columns,
dataList,
treeData,
treeLoading,
selectedNum,
pagination,
buttonClass,
deviceDetection,
onSearch,
resetForm,
onbatchDel,
openDialog,
onTreeSelect,
handleUpdate,
handleDelete,
handleUpload,
handleReset,
handleRole,
handleSizeChange,
onSelectionCancel,
handleCurrentChange,
handleSelectionChange
} = useUser(tableRef, treeRef);
</script>
<template>
<div class="systems">
<h2>Systems</h2>
<slot />
<div :class="['flex', 'justify-between', deviceDetection() && 'flex-wrap']">
<!-- 部门树组件 -->
<tree
ref="treeRef"
:class="['mr-2', deviceDetection() ? 'w-full' : 'min-w-[200px]']"
:treeData="treeData"
:treeLoading="treeLoading"
@tree-select="onTreeSelect"
/>
<!-- 右侧内容区域,根据设备检测结果调整宽度 -->
<div
:class="[deviceDetection() ? ['w-full', 'mt-2'] : 'w-[calc(100%-200px)]']"
>
<!-- 搜索表单 -->
<el-form
ref="formRef"
:inline="true"
:model="form"
class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
>
<!-- 用户名称输入项 -->
<el-form-item label="用户名称:" prop="username">
<el-input
v-model="form.username"
placeholder="请输入用户名称"
clearable
class="w-[180px]!"
/>
</el-form-item>
<!-- 手机号码输入项 -->
<el-form-item label="手机号码:" prop="mobile">
<el-input
v-model="form.mobile"
placeholder="请输入手机号码"
clearable
class="w-[180px]!"
/>
</el-form-item>
<!-- 用户状态选择项 -->
<el-form-item label="状态:" prop="status">
<el-select
v-model="form.status"
placeholder="请选择"
clearable
class="w-[180px]!"
>
<el-option label="已开启" value="1" />
<el-option label="已关闭" value="0" />
</el-select>
</el-form-item>
<!-- 操作按钮项 -->
<el-form-item>
<!-- 搜索按钮,点击触发搜索操作 -->
<el-button
type="primary"
:icon="useRenderIcon('ri/search-line')"
:loading="loading"
@click="onSearch"
>
搜索
</el-button>
<!-- 重置按钮,点击触发重置表单并重新搜索 -->
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
重置
</el-button>
</el-form-item>
</el-form>
<!-- 自定义表格栏组件 -->
<PureTableBar
title="用户管理(仅演示,操作后不生效)"
:columns="columns"
@refresh="onSearch"
>
<!-- 自定义按钮插槽,放置新增用户按钮 -->
<template #buttons>
<el-button
type="primary"
:icon="useRenderIcon(AddFill)"
@click="openDialog()"
>
新增用户
</el-button>
</template>
<!-- 表格内容插槽 -->
<template v-slot="{ size, dynamicColumns }">
<!-- 当有选中记录时显示提示信息和批量操作按钮 -->
<div
v-if="selectedNum > 0"
v-motion-fade
class="bg-[var(--el-fill-color-light)] w-full h-[46px] mb-2 pl-4 flex items-center"
>
<div class="flex-auto">
<!-- 显示已选记录数量 -->
<span
style="font-size: var(--el-font-size-base)"
class="text-[rgba(42,46,54,0.5)] dark:text-[rgba(220,220,242,0.5)]"
>
已选 {{ selectedNum }}
</span>
<!-- 取消选择按钮,点击触发取消选择操作 -->
<el-button type="primary" text @click="onSelectionCancel">
取消选择
</el-button>
</div>
<!-- 删除确认弹窗,确认后触发批量删除操作 -->
<el-popconfirm title="是否确认删除?" @confirm="onbatchDel">
<template #reference>
<el-button type="danger" text class="mr-1!">
批量删除
</el-button>
</template>
</el-popconfirm>
</div>
<!-- 自定义表格组件 -->
<pure-table
ref="tableRef"
row-key="id"
adaptive
:adaptiveConfig="{ offsetBottom: 108 }"
align-whole="center"
table-layout="auto"
:loading="loading"
:size="size"
:data="dataList"
:columns="dynamicColumns"
:pagination="{ ...pagination, size }"
:header-cell-style="{
background: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)'
}"
@selection-change="handleSelectionChange"
@page-size-change="handleSizeChange"
@page-current-change="handleCurrentChange"
>
<!-- 操作列插槽,放置针对每条记录的操作按钮 -->
<template #operation="{ row }">
<!-- 编辑按钮,点击打开编辑用户对话框 -->
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(EditPen)"
@click="openDialog('修改', row)"
>
修改
</el-button>
<!-- 删除确认弹窗,确认后触发删除单个用户操作 -->
<!-- <el-popconfirm
:title="`是否确认删除用户编号为${row.id}的这条数据`"
@confirm="handleDelete(row)"
>
<template #reference> -->
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(Delete)"
@click="handleDelete(row)"
>
删除
</el-button>
<!-- </template>
</el-popconfirm> -->
<el-dropdown>
<el-button
class="ml-3! mt-[2px]!"
link
type="primary"
:size="size"
:icon="useRenderIcon(More)"
@click="handleUpdate(row)"
/>
<template #dropdown>
<el-dropdown-menu>
<!-- 上传头像菜单项,点击触发上传头像操作 -->
<el-dropdown-item>
<el-button
:class="buttonClass"
link
type="primary"
:size="size"
:icon="useRenderIcon(Upload)"
@click="handleUpload(row)"
>
上传头像
</el-button>
</el-dropdown-item>
<!-- 重置密码菜单项,点击触发重置密码操作 -->
<el-dropdown-item>
<el-button
:class="buttonClass"
link
type="primary"
:size="size"
:icon="useRenderIcon(Password)"
@click="handleReset(row)"
>
重置密码
</el-button>
</el-dropdown-item>
<!-- 分配角色菜单项,点击触发分配角色操作 -->
<el-dropdown-item>
<el-button
:class="buttonClass"
link
type="primary"
:size="size"
:icon="useRenderIcon(Role)"
@click="handleRole(row)"
>
分配角色
</el-button>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</pure-table>
</template>
</PureTableBar>
</div>
</div>
</template>
<script setup lang="ts">
// 组件逻辑部分
import { ref } from "vue";
<style lang="scss" scoped>
/* 设置下拉菜单中图标元素的外边距为 0 */
:deep(.el-dropdown-menu__item i) {
margin: 0;
}
const count = ref(0);
/* 设置按钮获得焦点时不显示轮廓 */
:deep(.el-button:focus-visible) {
outline: none;
}
const increment = () => {
count.value++;
};
</script>
/* 主内容区域样式,设置外边距 */
.main-content {
margin: 24px 24px 0 !important;
}
<style scoped lang="scss">
.systems {
padding: 20px;
border: 1px solid #ccc;
/* 搜索表单样式,设置内部表单项的底部外边距 */
.search-form {
:deep(.el-form-item) {
margin-bottom: 12px;
}
}
</style>
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M22 4V2H2v2h9v14.17l-5.5-5.5-1.42 1.41L12 22l7.92-7.92-1.42-1.41-5.5 5.5V4z"/></svg>
\ No newline at end of file
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M4 2H2v20h2v-9h14.17l-5.5 5.5 1.41 1.42L22 12l-7.92-7.92-1.41 1.42 5.5 5.5H4z"/></svg>
\ No newline at end of file
<script setup lang="ts">
// 引入图标渲染 hook
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
// 从 Vue 引入响应式 API、监听 API 和获取当前实例的函数
import { ref, computed, watch, getCurrentInstance } from "vue";
// 引入图标组件
import Dept from "~icons/ri/git-branch-line";
// import Reset from "~icons/ri/restart-line";
import More2Fill from "~icons/ri/more-2-fill?width=18&height=18";
import OfficeBuilding from "~icons/ep/office-building";
import LocationCompany from "~icons/ep/add-location";
import ExpandIcon from "./svg/expand.svg?component";
import UnExpandIcon from "./svg/unexpand.svg?component";
/**
* 定义树形结构节点的接口
* 描述了树形结构中每个节点的属性结构
*/
interface Tree {
id: number; // 节点唯一标识
name: string; // 节点显示名称
highlight?: boolean; // 节点是否高亮,可选属性
children?: Tree[]; // 子节点数组,可选属性
}
/**
* 定义组件的 props
* 接收父组件传递的部门树加载状态和部门树数据
*/
defineProps({
treeLoading: Boolean, // 部门树是否正在加载
treeData: Array // 部门树数据数组
});
/**
* 定义组件的自定义事件
* 当部门树节点被选择时触发 `tree-select` 事件
*/
const emit = defineEmits(["tree-select"]);
// 创建树形组件的引用
const treeRef = ref();
// 记录树形组件是否全部展开的状态
const isExpand = ref(true);
// 记录搜索框的输入值
const searchValue = ref("");
// 记录节点高亮状态的映射对象
const highlightMap = ref({});
// 获取当前组件实例
const { proxy } = getCurrentInstance();
/**
* 定义树形组件的默认属性
* 指定树形组件中表示子节点和显示标签的字段名
*/
const defaultProps = {
children: "children", // 表示子节点的字段名
label: "name" // 表示显示标签的字段名
};
/**
* 计算按钮的样式类
* 返回一个包含多个 CSS 类名的数组,用于设置按钮的样式
*/
const buttonClass = computed(() => {
return [
"h-[20px]!",
"text-sm!",
"reset-margin",
"text-(--el-text-color-regular)!",
"dark:text-white!",
"dark:hover:text-primary!"
];
});
/**
* 过滤树形节点的方法
* 根据搜索框输入值过滤树形节点,包含输入值的节点将显示
* @param value - 搜索框输入的过滤值
* @param data - 当前树形节点的数据
* @returns 如果节点包含过滤值或过滤值为空则返回 true,否则返回 false
*/
const filterNode = (value: string, data: Tree) => {
if (!value) return true; // 过滤值为空,显示所有节点
return data.name.includes(value); // 节点名称包含过滤值则显示
};
/**
* 处理树形节点点击事件
* 切换点击节点的高亮状态,并将选中状态通过事件传递给父组件
* @param value - 点击的节点数据
*/
function nodeClick(value) {
const nodeId = value.$treeNodeId; // 获取节点的唯一标识
// 切换当前节点的高亮状态
highlightMap.value[nodeId] = highlightMap.value[nodeId]?.highlight
? Object.assign({ id: nodeId }, highlightMap.value[nodeId], {
highlight: false
})
: Object.assign({ id: nodeId }, highlightMap.value[nodeId], {
highlight: true
});
// 取消其他节点的高亮状态
Object.values(highlightMap.value).forEach((v: Tree) => {
if (v.id !== nodeId) {
v.highlight = false;
}
});
// 触发 `tree-select` 事件,传递选中节点的信息
emit(
"tree-select",
highlightMap.value[nodeId]?.highlight
? Object.assign({ ...value, selected: true })
: Object.assign({ ...value, selected: false })
);
}
/**
* 展开或折叠所有树形节点
* 根据传入的状态设置所有节点的展开状态
* @param status - 展开或折叠状态,true 为展开,false 为折叠
*/
function toggleRowExpansionAll(status) {
isExpand.value = status; // 更新展开状态记录
const nodes = (proxy.$refs["treeRef"] as any).store._getAllNodes(); // 获取所有节点
for (let i = 0; i < nodes.length; i++) {
nodes[i].expanded = status; // 设置节点的展开状态
}
}
/**
* 重置部门树状态
* 清空节点高亮状态、搜索框内容,并展开所有节点
*/
function onTreeReset() {
highlightMap.value = {}; // 清空节点高亮状态
searchValue.value = ""; // 清空搜索框内容
toggleRowExpansionAll(true); // 展开所有节点
}
/**
* 监听搜索框输入值的变化
* 当搜索框输入值变化时,触发树形组件的过滤方法
*/
watch(searchValue, val => {
treeRef.value!.filter(val); // 调用树形组件的过滤方法
});
/**
* 将重置部门树状态的方法暴露给父组件
* 允许父组件调用 `onTreeReset` 方法重置部门树状态
*/
defineExpose({ onTreeReset });
</script>
<template>
<!-- 容器元素,根据 `treeLoading` 显示加载状态,设置背景、高度等样式 -->
<div
v-loading="treeLoading"
class="h-full bg-bg_color overflow-hidden relative"
:style="{ minHeight: `calc(100vh - 141px)` }"
>
<!-- 搜索框和操作按钮容器 -->
<div class="flex items-center h-[34px]">
<!-- 搜索框组件,绑定搜索框输入值,设置占位提示和清空按钮 -->
<el-input
v-model="searchValue"
class="ml-2"
size="small"
placeholder="请输入部门名称"
clearable
>
<!-- 搜索框后缀图标,输入为空时显示搜索图标 -->
<template #suffix>
<el-icon class="el-input__icon">
<IconifyIconOffline
v-show="searchValue.length === 0"
icon="ri/search-line"
/>
</el-icon>
</template>
</el-input>
<!-- 下拉菜单组件,提供展开/折叠和重置状态操作 -->
<el-dropdown :hide-on-click="false">
<!-- 下拉菜单触发图标 -->
<More2Fill class="w-[28px] cursor-pointer outline-hidden" />
<!-- 下拉菜单内容 -->
<template #dropdown>
<el-dropdown-menu>
<!-- 展开/折叠菜单项 -->
<el-dropdown-item>
<el-button
:class="buttonClass"
link
type="primary"
:icon="useRenderIcon(isExpand ? ExpandIcon : UnExpandIcon)"
@click="toggleRowExpansionAll(isExpand ? false : true)"
>
<!-- 根据展开状态显示不同的文本 -->
{{ isExpand ? "折叠全部" : "展开全部" }}
</el-button>
</el-dropdown-item>
<!-- 注释掉的重置状态菜单项 -->
<!-- <el-dropdown-item>
<el-button
:class="buttonClass"
link
type="primary"
:icon="useRenderIcon(Reset)"
@click="onTreeReset"
>
重置状态
</el-button>
</el-dropdown-item> -->
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<!-- 分割线 -->
<el-divider />
<!-- 滚动条组件,设置高度 -->
<el-scrollbar height="calc(90vh - 88px)">
<!-- 树形组件,绑定数据、属性和事件 -->
<el-tree
ref="treeRef"
:data="treeData"
node-key="id"
size="small"
:props="defaultProps"
default-expand-all
:expand-on-click-node="false"
:filter-node-method="filterNode"
@node-click="nodeClick"
>
<!-- 树形节点默认内容模板 -->
<template #default="{ node, data }">
<div
:class="[
'rounded-sm',
'flex',
'items-center',
'select-none',
'hover:text-primary',
searchValue.trim().length > 0 &&
node.label.includes(searchValue) &&
'text-red-500',
highlightMap[node.id]?.highlight ? 'dark:text-primary' : ''
]"
:style="{
color: highlightMap[node.id]?.highlight
? 'var(--el-color-primary)'
: '',
background: highlightMap[node.id]?.highlight
? 'var(--el-color-primary-light-7)'
: 'transparent'
}"
>
<!-- 根据节点类型显示不同的图标 -->
<IconifyIconOffline
:icon="
data.type === 1
? OfficeBuilding
: data.type === 2
? LocationCompany
: Dept
"
/>
<!-- 显示节点名称,超出部分用省略号表示 -->
<span class="w-[120px]! truncate!" :title="node.label">
{{ node.label }}
</span>
</div>
</template>
</el-tree>
</el-scrollbar>
</div>
</template>
<style lang="scss" scoped>
/* 设置分割线的外边距为 0 */
:deep(.el-divider) {
margin: 0;
}
/* 设置树形组件节点悬停时的背景色为透明 */
:deep(.el-tree) {
--el-tree-node-hover-bg-color: transparent;
}
</style>
import "./reset.css";
// import dayjs from "dayjs";
import roleForm from "../form/role.vue";
import editForm from "../form/index.vue";
import { zxcvbn } from "@zxcvbn-ts/core";
import { handleTree } from "@/utils/tree";
import { message } from "@/utils/message";
import userAvatar from "@/assets/user.jpg";
import { usePublicHooks } from "../../hooks";
import { addDialog } from "@/components/ReDialog";
import type { PaginationProps } from "@pureadmin/table";
import ReCropperPreview from "@/components/ReCropperPreview";
import type { FormItemProps, RoleFormItemProps } from "../utils/types";
import {
getKeyList,
isAllEmpty,
// hideTextAtIndex,
deviceDetection
} from "@pureadmin/utils";
import {
addUser,
deleteUser,
updateUser,
getUserList,
addUserRole,
getDeptList,
getRoleListNoPage,
getRoleListByUserId
} from "@/api/systems";
import {
ElForm,
ElInput,
ElFormItem,
ElProgress,
ElMessageBox
} from "element-plus";
import {
type Ref,
h,
ref,
toRaw,
watch,
computed,
reactive,
onMounted
} from "vue";
// import { id } from "element-plus/es/locale/index.mjs";
export function useUser(tableRef: Ref, treeRef: Ref) {
const form = reactive({
// 左侧部门树的id
deptId: null,
username: null,
mobile: null,
status: null,
pageNum: 1,
pageSize: 10
});
const formRef = ref();
const ruleFormRef = ref();
const dataList = ref([]);
const loading = ref(true);
// 上传头像信息
const avatarInfo = ref();
const switchLoadMap = ref({});
const { switchStyle } = usePublicHooks();
const higherDeptOptions = ref();
const treeData = ref([]);
const treeLoading = ref(true);
const selectedNum = ref(0);
const pagination = reactive<PaginationProps>({
total: 0,
pageSize: 10,
currentPage: 1,
background: true
});
const columns: TableColumnList = [
{
label: "勾选列", // 如果需要表格多选,此处label必须设置
type: "selection",
fixed: "left",
reserveSelection: true // 数据刷新后保留选项
},
{
label: "用户编号",
prop: "id",
width: 90
},
{
label: "用户头像",
prop: "avatar",
cellRenderer: ({ row }) => (
<el-image
fit="cover"
preview-teleported={true}
src={row.avatar || userAvatar}
preview-src-list={Array.of(row.avatar || userAvatar)}
class="w-[24px] h-[24px] rounded-full align-middle"
/>
),
width: 90
},
{
label: "用户名称",
prop: "username",
minWidth: 130
},
{
label: "用户昵称",
prop: "nickname",
minWidth: 130
},
// {
// label: "性别",
// prop: "gender",
// minWidth: 90,
// cellRenderer: ({ row, props }) => (
// <el-tag
// size={props.size}
// type={row.gender === 1 ? "danger" : null}
// effect="plain"
// >
// {row.gender === 1 ? "女" : "男"}
// </el-tag>
// )
// },
// {
// label: "部门",
// prop: "dept.name",
// minWidth: 90
// },
{
label: "手机号码",
prop: "mobile",
minWidth: 90
// formatter: ({ mobile }) => hideTextAtIndex(mobile, { start: 3, end: 6 })
},
{
label: "状态",
prop: "status",
minWidth: 90,
cellRenderer: scope => (
<el-switch
size={scope.props.size === "small" ? "small" : "default"}
loading={switchLoadMap.value[scope.index]?.loading}
v-model={scope.row.status}
active-value={1}
inactive-value={0}
active-text="已启用"
inactive-text="已停用"
inline-prompt
style={switchStyle.value}
onChange={() => onChange(scope as any)}
/>
)
},
// {
// label: "创建时间",
// minWidth: 90,
// prop: "createTime",
// formatter: ({ createTime }) =>
// dayjs(createTime).format("YYYY-MM-DD HH:mm:ss")
// },
{
label: "操作",
fixed: "right",
width: 180,
slot: "operation"
}
];
const buttonClass = computed(() => {
return [
"h-[20px]!",
"reset-margin",
"text-gray-500!",
"dark:text-white!",
"dark:hover:text-primary!"
];
});
// 重置的新密码
const pwdForm = reactive({
newPwd: ""
});
const pwdProgress = [
{ color: "#e74242", text: "非常弱" },
{ color: "#EFBD47", text: "弱" },
{ color: "#ffa500", text: "一般" },
{ color: "#1bbf1b", text: "强" },
{ color: "#008000", text: "非常强" }
];
// 当前密码强度(0-4)
const curScore = ref();
const roleOptions = ref([]);
function onChange({ row, index }) {
ElMessageBox.confirm(
`确认要<strong>${
row.status === 0 ? "停用" : "启用"
}</strong><strong style='color:var(--el-color-primary)'>${
row.username
}</strong>用户吗?`,
"系统提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
dangerouslyUseHTMLString: true,
draggable: true
}
)
.then(() => {
switchLoadMap.value[index] = Object.assign(
{},
switchLoadMap.value[index],
{
loading: true
}
);
setTimeout(() => {
switchLoadMap.value[index] = Object.assign(
{},
switchLoadMap.value[index],
{
loading: false
}
);
message("已成功修改用户状态", {
type: "success"
});
}, 300);
})
.catch(() => {
row.status === 0 ? (row.status = 1) : (row.status = 0);
});
}
function handleUpdate(row) {
console.log(row);
}
function handleDelete(row) {
// 添加删除功能
ElMessageBox.confirm(
`确认要删除<strong style='color:var(--el-color-primary)'>${
row.username
}</strong>用户吗?`,
"系统提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
dangerouslyUseHTMLString: true,
draggable: true
}
)
.then(() => {
deleteUser(row.id).then(res => {
if (res.code === "0") {
onSearch();
message(`您删除了用户编号为${row.id}的这条数据`, {
type: "success"
});
} else {
message("删除失败", { type: "error" });
}
});
})
.catch(() => {});
}
function handleSizeChange(val: number) {
console.log(`${val} items per page`);
}
function handleCurrentChange(val: number) {
console.log(`current page: ${val}`);
}
/** 当CheckBox选择项发生变化时会触发该事件 */
function handleSelectionChange(val) {
selectedNum.value = val.length;
// 重置表格高度
tableRef.value.setAdaptive();
}
/** 取消选择 */
function onSelectionCancel() {
selectedNum.value = 0;
// 用于多选表格,清空用户的选择
tableRef.value.getTableRef().clearSelection();
}
/** 批量删除 */
function onbatchDel() {
// 返回当前选中的行
const curSelected = tableRef.value.getTableRef().getSelectionRows();
// 接下来根据实际业务,通过选中行的某项数据,比如下面的id,调用接口进行批量删除
message(`已删除用户编号为 ${getKeyList(curSelected, "id")} 的数据`, {
type: "success"
});
tableRef.value.getTableRef().clearSelection();
onSearch();
}
async function onSearch() {
loading.value = true;
const { data } = await getUserList(toRaw(form));
dataList.value = data.records;
pagination.total = data.total;
pagination.pageSize = data.pageSize;
pagination.currentPage = data.currentPage;
setTimeout(() => {
loading.value = false;
}, 500);
}
const resetForm = formEl => {
if (!formEl) return;
formEl.resetFields();
form.deptId = "";
treeRef.value.onTreeReset();
onSearch();
};
function onTreeSelect({ id, selected }) {
form.deptId = selected ? id : "";
onSearch();
}
function formatHigherDeptOptions(treeList) {
// 根据返回数据的status字段值判断追加是否禁用disabled字段,返回处理后的树结构,用于上级部门级联选择器的展示(实际开发中也是如此,不可能前端需要的每个字段后端都会返回,这时需要前端自行根据后端返回的某些字段做逻辑处理)
if (!treeList || !treeList.length) return;
const newTreeList = [];
for (let i = 0; i < treeList.length; i++) {
treeList[i].disabled = treeList[i].status === 0 ? true : false;
formatHigherDeptOptions(treeList[i].children);
newTreeList.push(treeList[i]);
}
return newTreeList;
}
function openDialog(title = "新增", row?: FormItemProps) {
const depts = formatHigherDeptOptions(higherDeptOptions.value); // 处理上级部门数据,获取选择的部门层级数组
addDialog({
title: `${title}用户`,
props: {
formInline: {
title,
higherDeptOptions: formatHigherDeptOptions(higherDeptOptions.value),
// deptId: row?.deptId ?? 0,
deptId: (row?.deptId || depts[depts.length - 1].id) ?? 0,
dependencies: row?.deptId ?? 0,
nickname: row?.nickname ?? "",
username: row?.username ?? "",
password: row?.password ?? "",
mobile: row?.mobile ?? "",
email: row?.email ?? "",
gender: row?.gender ?? "",
status: row?.status ?? 1,
remark: row?.remark ?? "",
name: row?.username ?? "",
id: row?.id ?? 0,
avatar: row?.avatar ?? ""
}
},
width: "46%",
draggable: true,
fullscreen: deviceDetection(),
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => h(editForm, { ref: formRef, formInline: null }),
beforeSure: (done, { options }) => {
const FormRef = formRef.value.getRef();
const curData = options.props.formInline as FormItemProps;
function chores() {
message(`您${title}了用户名称为${curData.username}的这条数据`, {
type: "success"
});
done(); // 关闭弹框
onSearch(); // 刷新表格数据
}
FormRef.validate(valid => {
if (valid) {
console.log("curData", curData);
// 表单规则校验通过
const {
avatar,
deptId,
email,
gender,
mobile,
name,
nickname,
username,
higherDeptOptions
} = curData;
console.log("higherDeptOptions", higherDeptOptions);
const params: {
avatar: string;
deptId: number;
email: string;
gender: string | number;
mobile: string | number;
name: string;
nickname: string;
username: string;
id?: number; // Add optional id property
} = {
avatar: avatar,
deptId,
email,
gender,
mobile,
name: name || username,
nickname,
username
};
if (title === "新增") {
// 实际开发先调用新增接口,再进行下面操作
// 新增用户
addUser(params).then(res => {
if (res.code === "0") {
message("新增用户成功", { type: "success" });
chores();
} else {
message("新增用户失败", { type: "error" });
}
});
// chores();
} else {
// 实际开发先调用修改接口,再进行下面操作
// 修改用户
// curData.name = curData.username;
console.log("curData", curData);
params.id = curData.id;
updateUser(params).then(res => {
if (res.code === "0") {
message("修改用户成功", { type: "success" });
chores();
} else {
message("修改用户失败", { type: "error" });
}
});
// chores();
}
}
});
}
});
}
const cropRef = ref();
/** 上传头像 */
function handleUpload(row) {
addDialog({
title: "裁剪、上传头像",
width: "40%",
closeOnClickModal: false,
fullscreen: deviceDetection(),
contentRenderer: () =>
h(ReCropperPreview, {
ref: cropRef,
imgSrc: row.avatar || userAvatar,
onCropper: info => (avatarInfo.value = info)
}),
beforeSure: done => {
console.log("裁剪后的图片信息:", avatarInfo.value);
// 根据实际业务使用avatarInfo.value和row里的某些字段去调用上传头像接口即可
done(); // 关闭弹框
onSearch(); // 刷新表格数据
},
closeCallBack: () => cropRef.value.hidePopover()
});
}
watch(
pwdForm,
({ newPwd }) =>
(curScore.value = isAllEmpty(newPwd) ? -1 : zxcvbn(newPwd).score)
);
/** 重置密码 */
function handleReset(row) {
addDialog({
title: `重置 ${row.username} 用户的密码`,
width: "30%",
draggable: true,
closeOnClickModal: false,
fullscreen: deviceDetection(),
contentRenderer: () => (
<>
<ElForm ref={ruleFormRef} model={pwdForm}>
<ElFormItem
prop="newPwd"
rules={[
{
required: true,
message: "请输入新密码",
trigger: "blur"
}
]}
>
<ElInput
clearable
show-password
type="password"
v-model={pwdForm.newPwd}
placeholder="请输入新密码"
/>
</ElFormItem>
</ElForm>
<div class="my-4 flex">
{pwdProgress.map(({ color, text }, idx) => (
<div
class="w-[19vw]"
style={{ marginLeft: idx !== 0 ? "4px" : 0 }}
>
<ElProgress
striped
striped-flow
duration={curScore.value === idx ? 6 : 0}
percentage={curScore.value >= idx ? 100 : 0}
color={color}
stroke-width={10}
show-text={false}
/>
<p
class="text-center"
style={{ color: curScore.value === idx ? color : "" }}
>
{text}
</p>
</div>
))}
</div>
</>
),
closeCallBack: () => (pwdForm.newPwd = ""),
beforeSure: done => {
ruleFormRef.value.validate(valid => {
if (valid) {
// 表单规则校验通过
message(`已成功重置 ${row.username} 用户的密码`, {
type: "success"
});
console.log(pwdForm.newPwd);
// 根据实际业务使用pwdForm.newPwd和row里的某些字段去调用重置用户密码接口即可
done(); // 关闭弹框
onSearch(); // 刷新表格数据
}
});
}
});
}
/** 分配角色 */
async function handleRole(row) {
// TODO 选中的角色列表
const ids = (await getRoleListByUserId(row.id)).data?.map(i => i.id) ?? [];
addDialog({
title: `分配 ${row.username} 用户的角色`,
props: {
formInline: {
username: row?.username ?? "",
nickname: row?.nickname ?? "",
roleOptions: roleOptions.value ?? [],
ids
}
},
width: "400px",
draggable: true,
fullscreen: deviceDetection(),
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => h(roleForm),
beforeSure: (done, { options }) => {
const curData = options.props.formInline as RoleFormItemProps;
console.log("curIds", curData.ids);
// 根据实际业务使用curData.ids和row里的某些字段去调用修改角色接口即可
// 调用用户绑定角色接口
addUserRole({
userId: row.id,
roleIds: curData.ids
}).then(res => {
if (res.code === "0") {
message("分配角色成功", { type: "success" });
done(); // 关闭弹框
onSearch(); // 刷新表格数据
} else {
message("分配角色失败", { type: "error" });
}
});
done(); // 关闭弹框
}
});
}
onMounted(async () => {
treeLoading.value = true;
onSearch();
// 归属部门
const { data } = await getDeptList({ pageNum: 1, pageSize: 200 });
higherDeptOptions.value = handleTree(data.records);
treeData.value = handleTree(data.records);
treeLoading.value = false;
// 角色列表
roleOptions.value = (await getRoleListNoPage()).data ?? [];
});
return {
form,
loading,
columns,
dataList,
treeData,
treeLoading,
selectedNum,
pagination,
buttonClass,
deviceDetection,
onSearch,
resetForm,
onbatchDel,
openDialog,
onTreeSelect,
handleUpdate,
handleDelete,
handleUpload,
handleReset,
handleRole,
handleSizeChange,
onSelectionCancel,
handleCurrentChange,
handleSelectionChange
};
}
/** 局部重置 ElProgress 的部分样式 */
.el-progress-bar__outer,
.el-progress-bar__inner {
border-radius: 0;
}
// 从 Vue 中引入 reactive 函数,用于创建响应式对象
import { reactive } from "vue";
// 从 Element Plus 引入 FormRules 类型,该类型用于定义表单验证规则
import type { FormRules } from "element-plus";
// 从 @pureadmin/utils 包中引入 isPhone 和 isEmail 工具函数,分别用于验证手机号和邮箱格式
import { isPhone, isEmail } from "@pureadmin/utils";
/**
* 自定义表单规则校验
* 该常量定义了用户表单中各个字段的验证规则,使用 reactive 函数将其转换为响应式对象,
* 方便在表单验证过程中动态更新规则。
*/
export const formRules = reactive<FormRules>({
// 昵称字段验证规则
nickname: [
{
required: true, // 该字段为必填项
message: "用户昵称为必填项", // 验证失败时的提示信息
trigger: "blur" // 触发验证的时机为失去焦点时
}
],
// 用户名字段验证规则
username: [
{
required: true, // 该字段为必填项
message: "用户名称为必填项", // 验证失败时的提示信息
trigger: "blur" // 触发验证的时机为失去焦点时
}
],
// 密码字段验证规则
password: [
{
required: true, // 该字段为必填项
message: "用户密码为必填项", // 验证失败时的提示信息
trigger: "blur" // 触发验证的时机为失去焦点时
}
],
// 手机号字段验证规则
phone: [
{
validator: (rule, value, callback) => {
if (value === "") {
// 如果手机号为空,则不进行格式验证,直接通过
callback();
} else if (!isPhone(value)) {
// 如果手机号不为空且格式不正确,返回错误信息
callback(new Error("请输入正确的手机号码格式"));
} else {
// 如果手机号不为空且格式正确,通过验证
callback();
}
},
trigger: "blur" // 触发验证的时机为失去焦点时
// trigger: "click" // 如果想在点击确定按钮时触发这个校验,trigger 设置成 click 即可
}
],
// 邮箱字段验证规则
email: [
{
validator: (rule, value, callback) => {
if (value === "") {
// 如果邮箱为空,则不进行格式验证,直接通过
callback();
} else if (!isEmail(value)) {
// 如果邮箱不为空且格式不正确,返回错误信息
callback(new Error("请输入正确的邮箱格式"));
} else {
// 如果邮箱不为空且格式正确,通过验证
callback();
}
},
trigger: "blur" // 触发验证的时机为失去焦点时
}
]
});
/**
* 定义用户表单单项属性的接口
* 此接口描述了用户表单中单个表单项的属性结构
*/
interface FormItemProps {
/** 用户 ID,可选字段,类型为数字 */
id?: number;
/** 用于判断是 `新增` 还是 `修改` 操作,类型为字符串 */
title: string;
/** 上级部门选项数组,每个选项是一个键值对对象,类型为 Record<string, unknown> 数组 */
higherDeptOptions: Record<string, unknown>[];
/** 父部门的 ID,类型为数字 */
parentId: number;
/** 用户昵称,类型为字符串 */
nickname: string;
/** 用户名,类型为字符串 */
username: string;
/** 用户密码,类型为字符串 */
password: string;
/** 用户手机号,类型可以是字符串或数字 */
mobile: string | number;
/** 用户邮箱,类型为字符串 */
email: string;
/** 用户性别,类型可以是字符串或数字 */
gender: string | number;
/** 用户状态,类型为数字 */
status: number;
/** 用户所属部门信息,可选字段,包含部门 ID 和部门名称 */
dept?: {
/** 部门 ID,可选字段,类型为数字 */
id?: number;
/** 部门名称,可选字段,类型为字符串 */
name?: string;
};
/** 备注信息,类型为字符串 */
remark: string;
/** 名称,可选字段,类型为字符串 */
name?: string;
/** 部门 ID,可选字段,类型为数字 */
deptId?: number;
/** 用户头像地址,可选字段,类型为字符串 */
avatar?: string;
}
/**
* 定义用户表单属性的接口
* 此接口描述了整个用户表单的属性结构,包含一个 FormItemProps 类型的表单内联对象
*/
interface FormProps {
/** 表单内联对象,包含用户表单的各项属性 */
formInline: FormItemProps;
}
/**
* 定义角色表单单项属性的接口
* 此接口描述了角色表单中单个表单项的属性结构
*/
interface RoleFormItemProps {
/** 用户名,类型为字符串 */
username: string;
/** 用户昵称,类型为字符串 */
nickname: string;
/** 角色列表,类型为任意类型的数组 */
roleOptions: any[];
/** 选中的角色列表,每个元素是一个以数字为键的键值对对象数组 */
ids: Record<number, unknown>[];
}
/**
* 定义角色表单属性的接口
* 此接口描述了整个角色表单的属性结构,包含一个 RoleFormItemProps 类型的表单内联对象
*/
interface RoleFormProps {
/** 表单内联对象,包含角色表单的各项属性 */
formInline: RoleFormItemProps;
}
/**
* 导出定义的类型
* 导出 FormItemProps、FormProps、RoleFormItemProps 和 RoleFormProps 接口,
* 方便在其他文件中使用这些类型定义
*/
export type { FormItemProps, FormProps, RoleFormItemProps, RoleFormProps };
......@@ -28,7 +28,13 @@ export default ({ mode }: ConfigEnv): UserConfigExport => {
proxy: {
"/api": {
// 这里填写后端地址
target: "http://192.168.1.194:5001",
// 旭哥地址
// target: "http://192.168.1.180:5001",
// 服务器地址
target: "http://192.168.1.248:5001",
// 熊熊哥地址
// target: "http://192.168.0.57:5001",
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, "")
}
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论