feat:添加后台管理系统布局

This commit is contained in:
FalingCliff 2025-06-15 00:33:23 +08:00
parent 7abe5d116d
commit 2b885dc351
31 changed files with 2862 additions and 527 deletions

View File

@ -16,6 +16,9 @@
"format": "prettier --write src/"
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"ant-design-vue": "^4.2.6",
"echarts": "^5.6.0",
"pinia": "^3.0.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0"

View File

@ -8,6 +8,15 @@ importers:
.:
dependencies:
'@ant-design/icons-vue':
specifier: ^7.0.1
version: 7.0.1(vue@3.5.16(typescript@5.8.3))
ant-design-vue:
specifier: ^4.2.6
version: 4.2.6(vue@3.5.16(typescript@5.8.3))
echarts:
specifier: ^5.6.0
version: 5.6.0
pinia:
specifier: ^3.0.1
version: 3.0.3(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3))
@ -100,6 +109,17 @@ packages:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
'@ant-design/colors@6.0.0':
resolution: {integrity: sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==}
'@ant-design/icons-svg@4.4.2':
resolution: {integrity: sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==}
'@ant-design/icons-vue@7.0.1':
resolution: {integrity: sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==}
peerDependencies:
vue: '>=3.0.3'
'@antfu/utils@0.7.10':
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
@ -230,6 +250,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/runtime@7.27.6':
resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'}
@ -270,6 +294,16 @@ packages:
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'}
'@ctrl/tinycolor@3.6.1':
resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==}
engines: {node: '>=10'}
'@emotion/hash@0.9.2':
resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
'@emotion/unitless@0.8.1':
resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==}
'@esbuild/aix-ppc64@0.25.5':
resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==}
engines: {node: '>=18'}
@ -685,6 +719,9 @@ packages:
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
'@simonwep/pickr@1.8.2':
resolution: {integrity: sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==}
'@sindresorhus/merge-streams@4.0.0':
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'}
@ -983,6 +1020,12 @@ packages:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
ant-design-vue@4.2.6:
resolution: {integrity: sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==}
engines: {node: '>=12.22.0'}
peerDependencies:
vue: '>=3.2.0'
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
@ -990,10 +1033,16 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
array-tree-filter@2.1.0:
resolution: {integrity: sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
async-validator@4.2.5:
resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@ -1064,6 +1113,9 @@ packages:
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
engines: {node: '>=14'}
compute-scroll-into-view@1.0.20:
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@ -1083,6 +1135,9 @@ packages:
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
engines: {node: '>=12.13'}
core-js@3.43.0:
resolution: {integrity: sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@ -1103,6 +1158,9 @@ packages:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
de-indent@1.0.2:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
@ -1137,9 +1195,18 @@ packages:
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
engines: {node: '>=12'}
dom-align@1.12.4:
resolution: {integrity: sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==}
dom-scroll-into-view@2.0.1:
resolution: {integrity: sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
echarts@5.6.0:
resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
editorconfig@1.0.4:
resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==}
engines: {node: '>=14'}
@ -1455,6 +1522,10 @@ packages:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
is-plain-object@3.0.1:
resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==}
engines: {node: '>=0.10.0'}
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
@ -1563,12 +1634,19 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
loupe@3.1.3:
resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
@ -1634,6 +1712,9 @@ packages:
engines: {node: ^18 || >=20}
hasBin: true
nanopop@2.4.2:
resolution: {integrity: sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==}
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@ -1809,6 +1890,9 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
resize-observer-polyfill@1.5.1:
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@ -1842,6 +1926,9 @@ packages:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
scroll-into-view-if-needed@2.2.31:
resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==}
scule@1.3.0:
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
@ -1854,6 +1941,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
shallow-equal@1.2.1:
resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@ -1918,6 +2008,9 @@ packages:
strip-literal@3.0.0:
resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==}
stylis@4.3.6:
resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==}
superjson@2.2.2:
resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}
engines: {node: '>=16'}
@ -1933,6 +2026,10 @@ packages:
resolution: {integrity: sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==}
engines: {node: ^14.18.0 || >=16.0.0}
throttle-debounce@5.0.2:
resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
engines: {node: '>=12.22'}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@ -1984,6 +2081,9 @@ packages:
peerDependencies:
typescript: '>=4.8.4'
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@ -2185,6 +2285,12 @@ packages:
peerDependencies:
typescript: '>=5.0.0'
vue-types@3.0.2:
resolution: {integrity: sha512-IwUC0Aq2zwaXqy74h4WCvFCUtoV0iSWr0snWnE9TnU18S66GAQyqQbRf2qfJtUuiFsBf6qp0MEwdonlwznlcrw==}
engines: {node: '>=10.15.0'}
peerDependencies:
vue: ^3.0.0
vue@3.5.16:
resolution: {integrity: sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==}
peerDependencies:
@ -2197,6 +2303,9 @@ packages:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
warning@4.0.3:
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
@ -2277,6 +2386,9 @@ packages:
resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==}
engines: {node: '>=18'}
zrender@5.6.1:
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
snapshots:
'@ampproject/remapping@2.3.0':
@ -2284,6 +2396,18 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25
'@ant-design/colors@6.0.0':
dependencies:
'@ctrl/tinycolor': 3.6.1
'@ant-design/icons-svg@4.4.2': {}
'@ant-design/icons-vue@7.0.1(vue@3.5.16(typescript@5.8.3))':
dependencies:
'@ant-design/colors': 6.0.0
'@ant-design/icons-svg': 4.4.2
vue: 3.5.16(typescript@5.8.3)
'@antfu/utils@0.7.10': {}
'@asamuzakjp/css-color@3.2.0':
@ -2460,6 +2584,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/runtime@7.27.6': {}
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
@ -2503,6 +2629,12 @@ snapshots:
'@csstools/css-tokenizer@3.0.4': {}
'@ctrl/tinycolor@3.6.1': {}
'@emotion/hash@0.9.2': {}
'@emotion/unitless@0.8.1': {}
'@esbuild/aix-ppc64@0.25.5':
optional: true
@ -2782,6 +2914,11 @@ snapshots:
'@sec-ant/readable-stream@0.4.1': {}
'@simonwep/pickr@1.8.2':
dependencies:
core-js: 3.43.0
nanopop: 2.4.2
'@sindresorhus/merge-streams@4.0.0': {}
'@tsconfig/node22@22.0.2': {}
@ -3176,6 +3313,32 @@ snapshots:
ansi-styles@6.2.1: {}
ant-design-vue@4.2.6(vue@3.5.16(typescript@5.8.3)):
dependencies:
'@ant-design/colors': 6.0.0
'@ant-design/icons-vue': 7.0.1(vue@3.5.16(typescript@5.8.3))
'@babel/runtime': 7.27.6
'@ctrl/tinycolor': 3.6.1
'@emotion/hash': 0.9.2
'@emotion/unitless': 0.8.1
'@simonwep/pickr': 1.8.2
array-tree-filter: 2.1.0
async-validator: 4.2.5
csstype: 3.1.3
dayjs: 1.11.13
dom-align: 1.12.4
dom-scroll-into-view: 2.0.1
lodash: 4.17.21
lodash-es: 4.17.21
resize-observer-polyfill: 1.5.1
scroll-into-view-if-needed: 2.2.31
shallow-equal: 1.2.1
stylis: 4.3.6
throttle-debounce: 5.0.2
vue: 3.5.16(typescript@5.8.3)
vue-types: 3.0.2(vue@3.5.16(typescript@5.8.3))
warning: 4.0.3
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
@ -3183,8 +3346,12 @@ snapshots:
argparse@2.0.1: {}
array-tree-filter@2.1.0: {}
assertion-error@2.0.1: {}
async-validator@4.2.5: {}
balanced-match@1.0.2: {}
binary-extensions@2.3.0: {}
@ -3258,6 +3425,8 @@ snapshots:
commander@10.0.1: {}
compute-scroll-into-view@1.0.20: {}
concat-map@0.0.1: {}
confbox@0.1.8: {}
@ -3275,6 +3444,8 @@ snapshots:
dependencies:
is-what: 4.1.16
core-js@3.43.0: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@ -3295,6 +3466,8 @@ snapshots:
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
dayjs@1.11.13: {}
de-indent@1.0.2: {}
debug@4.4.1:
@ -3316,8 +3489,17 @@ snapshots:
define-lazy-prop@3.0.0: {}
dom-align@1.12.4: {}
dom-scroll-into-view@2.0.1: {}
eastasianwidth@0.2.0: {}
echarts@5.6.0:
dependencies:
tslib: 2.3.0
zrender: 5.6.1
editorconfig@1.0.4:
dependencies:
'@one-ini/wasm': 0.1.1
@ -3654,6 +3836,8 @@ snapshots:
is-plain-obj@4.1.0: {}
is-plain-object@3.0.1: {}
is-potential-custom-element-name@1.0.1: {}
is-stream@4.0.1: {}
@ -3764,10 +3948,16 @@ snapshots:
dependencies:
p-locate: 5.0.0
lodash-es@4.17.21: {}
lodash.merge@4.6.2: {}
lodash@4.17.21: {}
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
loupe@3.1.3: {}
lru-cache@10.4.3: {}
@ -3822,6 +4012,8 @@ snapshots:
nanoid@5.1.5: {}
nanopop@2.4.2: {}
natural-compare@1.4.0: {}
node-releases@2.0.19: {}
@ -3989,6 +4181,8 @@ snapshots:
dependencies:
picomatch: 2.3.1
resize-observer-polyfill@1.5.1: {}
resolve-from@4.0.0: {}
reusify@1.1.0: {}
@ -4035,12 +4229,18 @@ snapshots:
dependencies:
xmlchars: 2.2.0
scroll-into-view-if-needed@2.2.31:
dependencies:
compute-scroll-into-view: 1.0.20
scule@1.3.0: {}
semver@6.3.1: {}
semver@7.7.2: {}
shallow-equal@1.2.1: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@ -4095,6 +4295,8 @@ snapshots:
dependencies:
js-tokens: 9.0.1
stylis@4.3.6: {}
superjson@2.2.2:
dependencies:
copy-anything: 3.0.5
@ -4109,6 +4311,8 @@ snapshots:
dependencies:
'@pkgr/core': 0.2.7
throttle-debounce@5.0.2: {}
tinybench@2.9.0: {}
tinyexec@0.3.2: {}
@ -4148,6 +4352,8 @@ snapshots:
dependencies:
typescript: 5.8.3
tslib@2.3.0: {}
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
@ -4392,6 +4598,11 @@ snapshots:
'@vue/language-core': 2.2.10(typescript@5.8.3)
typescript: 5.8.3
vue-types@3.0.2(vue@3.5.16(typescript@5.8.3)):
dependencies:
is-plain-object: 3.0.1
vue: 3.5.16(typescript@5.8.3)
vue@3.5.16(typescript@5.8.3):
dependencies:
'@vue/compiler-dom': 3.5.16
@ -4406,6 +4617,10 @@ snapshots:
dependencies:
xml-name-validator: 5.0.0
warning@4.0.3:
dependencies:
loose-envify: 1.4.0
webidl-conversions@7.0.0: {}
webpack-virtual-modules@0.6.2: {}
@ -4461,3 +4676,7 @@ snapshots:
yocto-queue@0.1.0: {}
yoctocolors@2.1.1: {}
zrender@5.6.1:
dependencies:
tslib: 2.3.0

View File

@ -1,85 +1,21 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
// AdminLayout
</script>
<template>
<header>
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
<div class="wrapper">
<HelloWorld msg="You did it!" />
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</div>
</header>
<RouterView />
<router-view />
</template>
<style scoped>
header {
line-height: 1.5;
max-height: 100vh;
<style>
/* 全局样式 */
html, body {
margin: 0;
padding: 0;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
#app {
height: 100%;
}
</style>

View File

@ -1,86 +0,0 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -1,35 +0,0 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

17
src/components.d.ts vendored
View File

@ -8,15 +8,16 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
IconCommunity: typeof import('./components/icons/IconCommunity.vue')['default']
IconDocumentation: typeof import('./components/icons/IconDocumentation.vue')['default']
IconEcosystem: typeof import('./components/icons/IconEcosystem.vue')['default']
IconSupport: typeof import('./components/icons/IconSupport.vue')['default']
IconTooling: typeof import('./components/icons/IconTooling.vue')['default']
AdminLayout: typeof import('./components/layout/AdminLayout.vue')['default']
AdvancedForm: typeof import('./components/form/AdvancedForm.vue')['default']
AdvancedTable: typeof import('./components/table/AdvancedTable.vue')['default']
Breadcrumb: typeof import('./components/common/Breadcrumb.vue')['default']
FooterBar: typeof import('./components/layout/FooterBar.vue')['default']
HeaderNav: typeof import('./components/layout/HeaderNav.vue')['default']
MainContent: typeof import('./components/layout/MainContent.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
TheWelcome: typeof import('./components/TheWelcome.vue')['default']
WelcomeItem: typeof import('./components/WelcomeItem.vue')['default']
SideMenu: typeof import('./components/layout/SideMenu.vue')['default']
YourComponent: typeof import('./components/YourComponent.vue')['default']
}
}

View File

@ -1,41 +0,0 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

View File

@ -1,93 +0,0 @@
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>. If
you need to test your components and web pages, check out
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
and
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
/
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
<br />
More instructions are available in
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
(our official Discord server), or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also follow the official
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
Bluesky account or the
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
X account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>

View File

@ -1,87 +0,0 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

View File

@ -1,11 +0,0 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'
describe('HelloWorld', () => {
it('renders properly', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})
})

View File

@ -0,0 +1,74 @@
<template>
<a-breadcrumb class="breadcrumb">
<a-breadcrumb-item v-for="(item, index) in breadcrumbItems" :key="index">
<router-link v-if="item.path && index !== breadcrumbItems.length - 1" :to="item.path">
{{ item.title }}
</router-link>
<span v-else>{{ item.title }}</span>
</a-breadcrumb-item>
</a-breadcrumb>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
//
const breadcrumbItems = ref<{ title: string; path: string }[]>([])
//
const generateBreadcrumb = () => {
const matched = route.matched
const items: { title: string; path: string }[] = []
matched.forEach((record) => {
if (record.meta && record.meta.title) {
items.push({
title: record.meta.title as string,
path: record.path,
})
}
})
//
if (items.length === 0) {
const pathSnippets = route.path.split('/').filter((i) => i)
let path = ''
pathSnippets.forEach((snippet) => {
path += `/${snippet}`
const title = snippet.charAt(0).toUpperCase() + snippet.slice(1)
items.push({
title,
path,
})
})
//
if (items.length === 0) {
items.push({
title: '首页',
path: '/',
})
}
}
breadcrumbItems.value = items
}
//
watch(
() => route.path,
() => {
generateBreadcrumb()
},
{ immediate: true }
)
</script>
<style scoped>
.breadcrumb {
margin-left: 8px;
}
</style>

View File

@ -1,7 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@ -1,7 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@ -1,7 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@ -1,7 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@ -1,19 +0,0 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

View File

@ -0,0 +1,332 @@
<template>
<a-layout class="admin-layout">
<!-- 侧边栏 -->
<a-layout-sider
v-model:collapsed="collapsed"
:trigger="null"
collapsible
class="admin-sider"
>
<div class="logo">
<img src="@/assets/logo.svg" alt="Logo" />
<h1 v-show="!collapsed">Admin System</h1>
</div>
<a-menu
v-model:selectedKeys="selectedKeys"
v-model:openKeys="openKeys"
mode="inline"
theme="dark"
>
<template v-for="menu in menus" :key="menu.key">
<!-- 有子菜单的情况 -->
<template v-if="menu.children && menu.children.length > 0">
<a-sub-menu :key="menu.key">
<template #title>
<span>
<component :is="menu.icon" />
<span>{{ menu.title }}</span>
</span>
</template>
<a-menu-item
v-for="child in menu.children"
:key="child.key"
@click="() => navigateTo(child.path)"
>
<component :is="child.icon" />
<span>{{ child.title }}</span>
</a-menu-item>
</a-sub-menu>
</template>
<!-- 没有子菜单的情况 -->
<template v-else>
<a-menu-item :key="menu.key" @click="() => navigateTo(menu.path)">
<component :is="menu.icon" />
<span>{{ menu.title }}</span>
</a-menu-item>
</template>
</template>
</a-menu>
</a-layout-sider>
<!-- 内容区域 -->
<a-layout class="admin-right-layout">
<!-- 头部 -->
<a-layout-header class="admin-header">
<div class="header-left">
<menu-unfold-outlined
v-if="collapsed"
class="trigger"
@click="() => (collapsed = !collapsed)"
/>
<menu-fold-outlined
v-else
class="trigger"
@click="() => (collapsed = !collapsed)"
/>
<breadcrumb />
</div>
<div class="header-right">
<a-dropdown>
<a class="user-dropdown" @click.prevent>
<a-avatar>
<template #icon><user-outlined /></template>
</a-avatar>
<span class="username">管理员</span>
</a>
<template #overlay>
<a-menu>
<a-menu-item key="profile">
<user-outlined />
个人中心
</a-menu-item>
<a-menu-item key="settings">
<setting-outlined />
个人设置
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout" @click="handleLogout">
<logout-outlined />
退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</a-layout-header>
<!-- 内容 -->
<a-layout-content class="admin-content">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</a-layout-content>
<!-- 页脚 -->
<a-layout-footer class="admin-footer">
Admin System ©{{ new Date().getFullYear() }} Created by Your Company
</a-layout-footer>
</a-layout>
</a-layout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import {
MenuUnfoldOutlined,
MenuFoldOutlined,
DashboardOutlined,
SettingOutlined,
UserOutlined,
TeamOutlined,
MenuOutlined,
LogoutOutlined,
} from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import Breadcrumb from '@/components/common/Breadcrumb.vue'
const router = useRouter()
const route = useRoute()
//
const collapsed = ref(false)
//
const selectedKeys = ref<string[]>([])
const openKeys = ref<string[]>([])
//
const menus = reactive([
{
key: 'dashboard',
title: '仪表盘',
icon: DashboardOutlined,
path: '/dashboard',
},
{
key: 'system',
title: '系统管理',
icon: SettingOutlined,
path: '/system',
children: [
{
key: 'user',
title: '用户管理',
icon: UserOutlined,
path: '/system/user',
},
{
key: 'role',
title: '角色管理',
icon: TeamOutlined,
path: '/system/role',
},
{
key: 'menu',
title: '菜单管理',
icon: MenuOutlined,
path: '/system/menu',
},
],
},
])
//
const updateSelectedMenu = () => {
const paths = route.path.split('/')
const currentPath = paths[paths.length - 1]
//
selectedKeys.value = [currentPath]
//
if (paths.length > 2) {
openKeys.value = [paths[1]]
}
}
//
const navigateTo = (path: string) => {
router.push(path)
}
// 退
const handleLogout = () => {
localStorage.removeItem('token')
message.success('已退出登录')
router.push('/login')
}
//
watch(
() => route.path,
() => {
updateSelectedMenu()
}
)
onMounted(() => {
updateSelectedMenu()
})
</script>
<style scoped>
.admin-layout {
height: 100vh;
display: flex;
flex-direction: row;
overflow: hidden;
}
.admin-sider {
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
z-index: 10;
height: 100vh;
overflow-y: auto;
}
.admin-right-layout {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.logo {
height: 64px;
padding: 16px;
display: flex;
align-items: center;
overflow: hidden;
}
.logo img {
height: 32px;
margin-right: 8px;
}
.logo h1 {
color: white;
font-size: 18px;
margin: 0;
white-space: nowrap;
}
.admin-header {
background: #fff;
padding: 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
z-index: 9;
}
.header-left {
display: flex;
align-items: center;
}
.trigger {
font-size: 18px;
cursor: pointer;
transition: color 0.3s;
padding: 0 24px 0 0;
}
.trigger:hover {
color: #1890ff;
}
.header-right {
display: flex;
align-items: center;
}
.user-dropdown {
display: flex;
align-items: center;
cursor: pointer;
padding: 0 12px;
transition: all 0.3s;
}
.user-dropdown:hover {
background: rgba(0, 0, 0, 0.025);
}
.username {
margin-left: 8px;
}
.admin-content {
margin: 24px;
padding: 24px;
background: #fff;
flex: 1;
overflow-y: auto;
overflow-x: hidden;
position: relative;
height: 0; /* 让flex: 1生效 */
}
.admin-footer {
text-align: center;
padding: 16px 50px;
flex-shrink: 0;
background: #fff;
}
/* 路由过渡动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,52 @@
<template>
<a-layout-footer class="footer">
<div class="footer-content">
<div class="copyright">
Admin Template ©{{ currentYear }} Created by Your Company
</div>
<div class="links">
<a href="#">帮助</a>
<a href="#">隐私</a>
<a href="#">条款</a>
</div>
</div>
</a-layout-footer>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const currentYear = ref(new Date().getFullYear())
</script>
<style scoped>
.footer {
text-align: center;
background: #f0f2f5;
padding: 16px 50px;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.copyright {
color: rgba(0, 0, 0, 0.45);
}
.links {
display: flex;
gap: 16px;
}
.links a {
color: rgba(0, 0, 0, 0.45);
transition: color 0.3s;
}
.links a:hover {
color: #1890ff;
}
</style>

View File

@ -0,0 +1,105 @@
<template>
<a-layout-header class="header">
<div class="header-left">
<menu-unfold-outlined
v-if="collapsed"
class="trigger"
@click="() => (collapsed = !collapsed)"
/>
<menu-fold-outlined
v-else
class="trigger"
@click="() => (collapsed = !collapsed)"
/>
<span class="logo">Admin Template</span>
</div>
<div class="header-right">
<a-space>
<a-badge count="5">
<a-button type="text">
<template #icon><BellOutlined /></template>
</a-button>
</a-badge>
<a-dropdown>
<a-button type="text">
<template #icon><UserOutlined /></template>
Admin User
</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="profile">
<user-outlined />
个人信息
</a-menu-item>
<a-menu-item key="settings">
<setting-outlined />
设置
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout">
<logout-outlined />
退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-space>
</div>
</a-layout-header>
</template>
<script setup lang="ts">
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
BellOutlined,
UserOutlined,
SettingOutlined,
LogoutOutlined,
} from '@ant-design/icons-vue'
defineProps<{
collapsed: boolean
}>()
defineEmits(['update:collapsed'])
</script>
<style scoped>
.header {
padding: 0;
background: #fff;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
z-index: 1;
}
.header-left {
display: flex;
align-items: center;
}
.header-right {
padding-right: 24px;
}
.trigger {
padding: 0 24px;
font-size: 18px;
cursor: pointer;
transition: color 0.3s;
}
.trigger:hover {
color: #1890ff;
}
.logo {
font-size: 18px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<a-layout-content class="main-content">
<div class="content-header">
<a-breadcrumb>
<a-breadcrumb-item>首页</a-breadcrumb-item>
<a-breadcrumb-item>系统管理</a-breadcrumb-item>
<a-breadcrumb-item>用户管理</a-breadcrumb-item>
</a-breadcrumb>
<div class="content-title">
<h2>用户管理</h2>
</div>
</div>
<div class="content-container">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</a-layout-content>
</template>
<script setup lang="ts">
//
</script>
<style scoped>
.main-content {
margin: 24px;
padding: 24px;
background: #fff;
min-height: 280px;
border-radius: 4px;
}
.content-header {
margin-bottom: 24px;
}
.content-title {
margin-top: 8px;
}
.content-title h2 {
margin: 0;
font-size: 20px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
.content-container {
min-height: 500px;
}
/* 过渡动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,133 @@
<template>
<a-layout-sider
v-model:collapsed="collapsed"
:trigger="null"
collapsible
class="side-menu"
>
<div class="logo-container">
<img src="@/assets/logo.svg" alt="Logo" class="logo" />
<h1 v-if="!collapsed" class="title">Admin System</h1>
</div>
<a-menu
v-model:selectedKeys="selectedKeys"
v-model:openKeys="openKeys"
mode="inline"
theme="dark"
>
<a-menu-item key="dashboard">
<template #icon>
<dashboard-outlined />
</template>
<span>仪表盘</span>
</a-menu-item>
<a-sub-menu key="system">
<template #icon>
<setting-outlined />
</template>
<template #title>系统管理</template>
<a-menu-item key="user">
<template #icon>
<user-outlined />
</template>
<span>用户管理</span>
</a-menu-item>
<a-menu-item key="role">
<template #icon>
<team-outlined />
</template>
<span>角色管理</span>
</a-menu-item>
<a-menu-item key="menu">
<template #icon>
<menu-outlined />
</template>
<span>菜单管理</span>
</a-menu-item>
</a-sub-menu>
<a-sub-menu key="components">
<template #icon>
<appstore-outlined />
</template>
<template #title>组件示例</template>
<a-menu-item key="table">
<template #icon>
<table-outlined />
</template>
<span>表格</span>
</a-menu-item>
<a-menu-item key="form">
<template #icon>
<form-outlined />
</template>
<span>表单</span>
</a-menu-item>
<a-menu-item key="chart">
<template #icon>
<bar-chart-outlined />
</template>
<span>图表</span>
</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import {
DashboardOutlined,
SettingOutlined,
UserOutlined,
TeamOutlined,
MenuOutlined,
AppstoreOutlined,
TableOutlined,
FormOutlined,
BarChartOutlined,
} from '@ant-design/icons-vue'
const props = defineProps<{
collapsed: boolean
}>()
defineEmits(['update:collapsed'])
const selectedKeys = ref(['dashboard'])
const openKeys = ref(['system'])
</script>
<style scoped>
.side-menu {
height: 100vh;
position: fixed;
left: 0;
overflow: auto;
}
.logo-container {
height: 64px;
padding: 16px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
margin-bottom: 8px;
}
.logo {
height: 32px;
margin-right: 8px;
}
.title {
color: white;
font-size: 18px;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@ -1,7 +1,8 @@
import './assets/main.css'
import 'ant-design-vue/dist/reset.css';
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import Antd from 'ant-design-vue';
import App from './App.vue'
import router from './router'
@ -10,5 +11,6 @@ const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(Antd);
app.mount('#app')

View File

@ -1,23 +1,77 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import { createRouter, createWebHistory } from 'vue-router';
import AdminLayout from '@/components/layout/AdminLayout.vue';
const routes = [
{
path: '/login',
name: 'login',
component: () => import('@/views/Login.vue'),
meta: { requiresAuth: false }
},
{
path: '/',
component: AdminLayout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { title: '仪表盘', icon: 'dashboard' }
},
{
path: 'system',
name: 'system',
redirect: '/system/user',
meta: { title: '系统管理', icon: 'setting' },
children: [
{
path: 'user',
name: 'user',
component: () => import('@/views/system/UserManagement.vue'),
meta: { title: '用户管理', icon: 'user' }
},
{
path: 'role',
name: 'role',
component: () => import('@/views/system/RoleManagement.vue'),
meta: { title: '角色管理', icon: 'team' }
},
{
path: 'menu',
name: 'menu',
component: () => import('@/views/system/MenuManagement.vue'),
meta: { title: '菜单管理', icon: 'menu' }
}
]
}
]
},
// 404页面
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('@/views/NotFound.vue')
}
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue'),
},
],
})
routes,
});
export default router
// 路由守卫
router.beforeEach((to, from, next) => {
// 这里可以添加身份验证逻辑
// 例如:检查用户是否已登录,如果未登录且访问需要认证的页面,则重定向到登录页
const requiresAuth = to.matched.some(record => record.meta.requiresAuth !== false);
const isAuthenticated = localStorage.getItem('token'); // 简单的身份验证检查
if (requiresAuth && !isAuthenticated) {
next('/login');
} else {
next();
}
});
export default router;

View File

@ -1,15 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>

538
src/views/Dashboard.vue Normal file
View File

@ -0,0 +1,538 @@
<template>
<div class="dashboard">
<a-row :gutter="24">
<!-- 统计卡片 -->
<a-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6" class="card-col">
<a-card class="statistic-card">
<template #title>
<div class="card-title">
<team-outlined class="card-icon user-icon" />
<span>用户总数</span>
</div>
</template>
<div class="card-content">
<div class="statistic-value">{{ statistics.userCount }}</div>
<div class="statistic-footer">
<span class="trend-text up">
<arrow-up-outlined /> {{ statistics.userIncrease }}%
</span>
<span class="trend-time">较上周</span>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6" class="card-col">
<a-card class="statistic-card">
<template #title>
<div class="card-title">
<interaction-outlined class="card-icon visit-icon" />
<span>访问量</span>
</div>
</template>
<div class="card-content">
<div class="statistic-value">{{ statistics.visitCount }}</div>
<div class="statistic-footer">
<span class="trend-text down">
<arrow-down-outlined /> {{ statistics.visitDecrease }}%
</span>
<span class="trend-time">较昨日</span>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6" class="card-col">
<a-card class="statistic-card">
<template #title>
<div class="card-title">
<file-text-outlined class="card-icon order-icon" />
<span>订单数</span>
</div>
</template>
<div class="card-content">
<div class="statistic-value">{{ statistics.orderCount }}</div>
<div class="statistic-footer">
<span class="trend-text up">
<arrow-up-outlined /> {{ statistics.orderIncrease }}%
</span>
<span class="trend-time">较上月</span>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6" class="card-col">
<a-card class="statistic-card">
<template #title>
<div class="card-title">
<dollar-outlined class="card-icon revenue-icon" />
<span>收入</span>
</div>
</template>
<div class="card-content">
<div class="statistic-value">¥{{ statistics.revenue }}</div>
<div class="statistic-footer">
<span class="trend-text up">
<arrow-up-outlined /> {{ statistics.revenueIncrease }}%
</span>
<span class="trend-time">较上月</span>
</div>
</div>
</a-card>
</a-col>
</a-row>
<a-row :gutter="24" style="margin-top: 24px">
<!-- 图表区域 -->
<a-col :xs="24" :sm="24" :md="24" :lg="16" :xl="16" class="chart-col">
<a-card title="访问量趋势" :bordered="false">
<div ref="visitChart" class="chart-container"></div>
</a-card>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8" class="chart-col">
<a-card title="用户分布" :bordered="false">
<div ref="userChart" class="chart-container"></div>
</a-card>
</a-col>
</a-row>
<a-row :gutter="24" style="margin-top: 24px">
<!-- 最近活动 -->
<a-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12" class="activity-col">
<a-card title="最近活动" :bordered="false">
<a-list class="activity-list" :data-source="activities" :pagination="false">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
<template #avatar>
<a-avatar :style="{ backgroundColor: item.avatarColor }">
{{ item.avatar }}
</a-avatar>
</template>
<template #title>
<a href="#">{{ item.title }}</a>
</template>
<template #description>
<div>{{ item.description }}</div>
<div class="activity-time">{{ item.time }}</div>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
<!-- 快速操作 -->
<a-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12" class="quick-col">
<a-card title="快速操作" :bordered="false">
<div class="quick-actions">
<a-button type="primary" @click="showMessage('添加用户')">
<template #icon><user-add-outlined /></template>
添加用户
</a-button>
<a-button @click="showMessage('发布公告')">
<template #icon><notification-outlined /></template>
发布公告
</a-button>
<a-button @click="showMessage('系统设置')">
<template #icon><setting-outlined /></template>
系统设置
</a-button>
<a-button @click="showMessage('数据备份')">
<template #icon><cloud-upload-outlined /></template>
数据备份
</a-button>
<a-button @click="showMessage('生成报表')">
<template #icon><file-pdf-outlined /></template>
生成报表
</a-button>
<a-button @click="showMessage('发送邮件')">
<template #icon><mail-outlined /></template>
发送邮件
</a-button>
</div>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
import * as echarts from 'echarts/core'
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
} from 'echarts/components'
import { LineChart, PieChart } from 'echarts/charts'
import { UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
import {
TeamOutlined,
InteractionOutlined,
FileTextOutlined,
DollarOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
UserAddOutlined,
NotificationOutlined,
SettingOutlined,
CloudUploadOutlined,
FilePdfOutlined,
MailOutlined,
} from '@ant-design/icons-vue'
// ECharts
echarts.use([
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
LineChart,
PieChart,
CanvasRenderer,
UniversalTransition,
])
//
const statistics = reactive({
userCount: '1,286',
userIncrease: 12.5,
visitCount: '8,846',
visitDecrease: 2.3,
orderCount: '1,286',
orderIncrease: 3.8,
revenue: '23,648',
revenueIncrease: 8.4,
})
//
const activities = ref([
{
avatar: '张',
avatarColor: '#1890ff',
title: '张三 添加了新用户',
description: '添加了用户"李四"到系统',
time: '刚刚',
},
{
avatar: '王',
avatarColor: '#52c41a',
title: '王五 更新了系统设置',
description: '修改了系统邮件配置',
time: '2小时前',
},
{
avatar: '赵',
avatarColor: '#faad14',
title: '赵六 发布了新公告',
description: '关于系统维护的公告',
time: '5小时前',
},
{
avatar: '钱',
avatarColor: '#f5222d',
title: '钱七 删除了用户',
description: '删除了用户"测试账号"',
time: '1天前',
},
])
//
const visitChart = ref<HTMLElement | null>(null)
const userChart = ref<HTMLElement | null>(null)
//
let visitChartInstance: echarts.ECharts | null = null
let userChartInstance: echarts.ECharts | null = null
// 访
const initVisitChart = () => {
if (visitChart.value) {
visitChartInstance = echarts.init(visitChart.value)
const option = {
tooltip: {
trigger: 'axis',
},
legend: {
data: ['访问量', '用户量'],
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
},
yAxis: {
type: 'value',
},
series: [
{
name: '访问量',
type: 'line',
data: [120, 132, 101, 134, 90, 230, 210],
smooth: true,
lineStyle: {
width: 3,
color: '#1890ff',
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(24,144,255,0.3)',
},
{
offset: 1,
color: 'rgba(24,144,255,0)',
},
],
},
},
},
{
name: '用户量',
type: 'line',
data: [220, 182, 191, 234, 290, 330, 310],
smooth: true,
lineStyle: {
width: 3,
color: '#52c41a',
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(82,196,26,0.3)',
},
{
offset: 1,
color: 'rgba(82,196,26,0)',
},
],
},
},
},
],
}
visitChartInstance.setOption(option)
}
}
//
const initUserChart = () => {
if (userChart.value) {
userChartInstance = echarts.init(userChart.value)
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)',
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
data: ['华东', '华南', '华北', '西南', '其他'],
},
series: [
{
name: '用户分布',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: [
{ value: 335, name: '华东' },
{ value: 310, name: '华南' },
{ value: 234, name: '华北' },
{ value: 135, name: '西南' },
{ value: 154, name: '其他' },
],
},
],
}
userChartInstance.setOption(option)
}
}
//
const handleResize = () => {
visitChartInstance?.resize()
userChartInstance?.resize()
}
//
const showMessage = (action: string) => {
message.success(`你点击了"${action}"按钮`)
}
onMounted(() => {
//
initVisitChart()
initUserChart()
//
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
//
window.removeEventListener('resize', handleResize)
//
visitChartInstance?.dispose()
userChartInstance?.dispose()
})
</script>
<style scoped>
.dashboard {
padding: 0;
}
.card-col {
margin-bottom: 24px;
}
.statistic-card {
height: 100%;
}
.card-title {
display: flex;
align-items: center;
}
.card-icon {
font-size: 20px;
margin-right: 8px;
}
.user-icon {
color: #1890ff;
}
.visit-icon {
color: #52c41a;
}
.order-icon {
color: #faad14;
}
.revenue-icon {
color: #f5222d;
}
.card-content {
padding: 16px;
}
.statistic-value {
font-size: 24px;
font-weight: bold;
margin-bottom: 8px;
}
.statistic-footer {
display: flex;
align-items: center;
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
}
.trend-text {
margin-right: 8px;
display: flex;
align-items: center;
}
.trend-text.up {
color: #f5222d;
}
.trend-text.down {
color: #52c41a;
}
.chart-col {
margin-bottom: 24px;
}
.chart-container {
height: 350px;
}
.activity-col,
.quick-col {
margin-bottom: 24px;
}
.activity-time {
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
margin-top: 4px;
}
.quick-actions {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
@media (max-width: 768px) {
.quick-actions {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 576px) {
.quick-actions {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -1,9 +0,0 @@
<script setup lang="ts">
import TheWelcome from '../components/TheWelcome.vue'
</script>
<template>
<main>
<TheWelcome />
</main>
</template>

152
src/views/Login.vue Normal file
View File

@ -0,0 +1,152 @@
<template>
<div class="login-container">
<div class="login-form-wrapper">
<div class="login-header">
<img src="@/assets/logo.svg" alt="Logo" class="login-logo" />
<h1 class="login-title">Admin System</h1>
</div>
<a-form
:model="formState"
name="login"
class="login-form"
@finish="handleSubmit"
@finishFailed="handleFinishFailed"
>
<a-form-item
name="username"
:rules="[{ required: true, message: '请输入用户名!' }]"
>
<a-input v-model:value="formState.username" size="large" placeholder="用户名">
<template #prefix>
<user-outlined class="site-form-item-icon" />
</template>
</a-input>
</a-form-item>
<a-form-item
name="password"
:rules="[{ required: true, message: '请输入密码!' }]"
>
<a-input-password v-model:value="formState.password" size="large" placeholder="密码">
<template #prefix>
<lock-outlined class="site-form-item-icon" />
</template>
</a-input-password>
</a-form-item>
<a-form-item>
<a-row :gutter="8">
<a-col :span="12">
<a-checkbox v-model:checked="formState.remember">记住我</a-checkbox>
</a-col>
<a-col :span="12" style="text-align: right">
<a class="login-form-forgot" href="">忘记密码</a>
</a-col>
</a-row>
</a-form-item>
<a-form-item>
<a-button
type="primary"
html-type="submit"
class="login-form-button"
size="large"
:loading="loading"
>
登录
</a-button>
</a-form-item>
</a-form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const loading = ref(false)
interface FormState {
username: string
password: string
remember: boolean
}
const formState = reactive<FormState>({
username: '',
password: '',
remember: true,
})
const handleSubmit = (values: FormState) => {
loading.value = true
//
setTimeout(() => {
if (values.username === 'admin' && values.password === 'admin') {
message.success('登录成功')
localStorage.setItem('token', 'demo-token')
router.push('/dashboard')
} else {
message.error('用户名或密码错误')
}
loading.value = false
}, 1000)
}
const handleFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo)
}
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: #f0f2f5;
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
.login-form-wrapper {
width: 368px;
margin: 0 auto;
}
.login-header {
text-align: center;
margin-bottom: 40px;
}
.login-logo {
height: 44px;
margin-right: 8px;
vertical-align: middle;
}
.login-title {
font-size: 33px;
color: rgba(0, 0, 0, 0.85);
font-weight: 600;
display: inline-block;
vertical-align: middle;
}
.login-form {
max-width: 368px;
}
.login-form-forgot {
float: right;
}
.login-form-button {
width: 100%;
}
</style>

32
src/views/NotFound.vue Normal file
View File

@ -0,0 +1,32 @@
<template>
<div class="not-found">
<a-result
status="404"
title="404"
sub-title="抱歉,您访问的页面不存在。"
>
<template #extra>
<a-button type="primary" @click="goHome">返回首页</a-button>
</template>
</a-result>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
const goHome = () => {
router.push('/')
}
</script>
<style scoped>
.not-found {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@ -0,0 +1,389 @@
<template>
<div class="menu-management">
<div class="table-operations">
<a-space>
<a-button type="primary" @click="showAddMenuModal(null)">
<template #icon><plus-outlined /></template>
新增菜单
</a-button>
<a-button @click="expandAll">
<template #icon><folder-open-outlined /></template>
展开全部
</a-button>
<a-button @click="collapseAll">
<template #icon><folder-outlined /></template>
折叠全部
</a-button>
</a-space>
</div>
<a-table
:columns="columns"
:data-source="menus"
:loading="loading"
:pagination="false"
:expandable="{
defaultExpandAllRows: true,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'icon'">
<component :is="record.icon" />
</template>
<template v-else-if="column.key === 'type'">
<a-tag :color="record.type === 1 ? 'blue' : record.type === 2 ? 'green' : 'orange'">
{{ record.type === 1 ? '目录' : record.type === 2 ? '菜单' : '按钮' }}
</a-tag>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="record.status === 1 ? 'success' : 'error'">
{{ record.status === 1 ? '启用' : '禁用' }}
</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a @click="showAddMenuModal(record)" v-if="record.type !== 3">添加子项</a>
<a-divider type="vertical" v-if="record.type !== 3" />
<a @click="showEditMenuModal(record)">编辑</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此菜单吗?"
ok-text="确定"
cancel-text="取消"
@confirm="handleDelete(record)"
>
<a class="danger-link">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<!-- 菜单表单模态框 -->
<a-modal
v-model:visible="menuModalVisible"
:title="modalTitle"
@ok="handleMenuModalOk"
@cancel="handleMenuModalCancel"
:confirmLoading="modalLoading"
width="700px"
>
<a-form
ref="menuForm"
:model="menuForm"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="上级菜单">
<a-tree-select
v-model:value="menuForm.parentId"
:tree-data="menuTreeData"
:field-names="{ children: 'children', label: 'title', value: 'key' }"
placeholder="请选择上级菜单"
allow-clear
tree-default-expand-all
:disabled="!!parentMenu"
/>
</a-form-item>
<a-form-item label="菜单类型" name="type">
<a-radio-group v-model:value="menuForm.type">
<a-radio :value="1">目录</a-radio>
<a-radio :value="2">菜单</a-radio>
<a-radio :value="3">按钮</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="菜单名称" name="title">
<a-input v-model:value="menuForm.title" placeholder="请输入菜单名称" />
</a-form-item>
<template v-if="menuForm.type !== 3">
<a-form-item label="图标" name="icon">
<a-input v-model:value="menuForm.icon" placeholder="请输入图标名称" />
</a-form-item>
<a-form-item label="路由地址" name="path" v-if="menuForm.type === 2">
<a-input v-model:value="menuForm.path" placeholder="请输入路由地址" />
</a-form-item>
<a-form-item label="组件路径" name="component" v-if="menuForm.type === 2">
<a-input v-model:value="menuForm.component" placeholder="请输入组件路径" />
</a-form-item>
</template>
<template v-else>
<a-form-item label="权限标识" name="permission">
<a-input v-model:value="menuForm.permission" placeholder="请输入权限标识" />
</a-form-item>
</template>
<a-form-item label="排序" name="sort">
<a-input-number v-model:value="menuForm.sort" :min="0" style="width: 100%" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-switch v-model:checked="menuForm.status" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { message } from 'ant-design-vue'
import {
PlusOutlined,
FolderOpenOutlined,
FolderOutlined,
DashboardOutlined,
SettingOutlined,
TeamOutlined,
MenuOutlined,
} from '@ant-design/icons-vue'
//
const columns = [
{
title: '菜单名称',
dataIndex: 'title',
key: 'title',
},
{
title: '图标',
dataIndex: 'icon',
key: 'icon',
width: 80,
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: 100,
},
{
title: '权限标识',
dataIndex: 'permission',
key: 'permission',
},
{
title: '路由地址',
dataIndex: 'path',
key: 'path',
},
{
title: '排序',
dataIndex: 'sort',
key: 'sort',
width: 80,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
},
{
title: '操作',
key: 'action',
width: 200,
},
]
//
const menus = ref([
{
key: '1',
title: '仪表盘',
icon: DashboardOutlined,
type: 2,
permission: 'dashboard',
path: '/dashboard',
component: '/views/Dashboard',
sort: 1,
status: 1,
},
{
key: '2',
title: '系统管理',
icon: SettingOutlined,
type: 1,
permission: 'system',
path: '/system',
sort: 2,
status: 1,
children: [
{
key: '2-1',
title: '用户管理',
icon: TeamOutlined,
type: 2,
permission: 'system:user',
path: '/system/user',
component: '/views/system/UserManagement',
sort: 1,
status: 1,
children: [
{
key: '2-1-1',
title: '查看用户',
type: 3,
permission: 'system:user:view',
sort: 1,
status: 1,
},
{
key: '2-1-2',
title: '新增用户',
type: 3,
permission: 'system:user:add',
sort: 2,
status: 1,
},
],
},
{
key: '2-2',
title: '菜单管理',
icon: MenuOutlined,
type: 2,
permission: 'system:menu',
path: '/system/menu',
component: '/views/system/MenuManagement',
sort: 2,
status: 1,
},
],
},
])
//
const loading = ref(false)
//
const menuModalVisible = ref(false)
const modalLoading = ref(false)
const modalTitle = ref('新增菜单')
const parentMenu = ref<any>(null)
//
const menuForm = reactive({
id: undefined,
parentId: undefined,
type: 1,
title: '',
icon: '',
path: '',
component: '',
permission: '',
sort: 0,
status: true,
})
//
const rules = {
title: [{ required: true, message: '请输入菜单名称' }],
type: [{ required: true, message: '请选择菜单类型' }],
permission: [{ required: true, message: '请输入权限标识', trigger: 'blur' }],
path: [{ required: true, message: '请输入路由地址', trigger: 'blur' }],
sort: [{ required: true, message: '请输入排序' }],
}
//
const menuTreeData = computed(() => {
const transform = (items: any[]): any[] => {
return items.map(item => ({
key: item.key,
title: item.title,
value: item.key,
disabled: item.type === 3,
children: item.children ? transform(item.children) : undefined,
}))
}
return [{ key: '0', title: '顶级菜单', value: '0' }, ...transform(menus.value)]
})
// /
const expandAll = () => {
//
message.success('已展开全部')
}
const collapseAll = () => {
//
message.success('已折叠全部')
}
//
const showAddMenuModal = (record: any) => {
modalTitle.value = '新增菜单'
parentMenu.value = record
Object.assign(menuForm, {
id: undefined,
parentId: record ? record.key : '0',
type: record ? (record.type === 1 ? 2 : 3) : 1,
title: '',
icon: '',
path: '',
component: '',
permission: '',
sort: 0,
status: true,
})
menuModalVisible.value = true
}
//
const showEditMenuModal = (record: any) => {
modalTitle.value = '编辑菜单'
parentMenu.value = null
Object.assign(menuForm, {
id: record.key,
parentId: record.parentId || '0',
type: record.type,
title: record.title,
icon: record.icon,
path: record.path,
component: record.component,
permission: record.permission,
sort: record.sort,
status: record.status === 1,
})
menuModalVisible.value = true
}
//
const handleMenuModalOk = () => {
modalLoading.value = true
//
setTimeout(() => {
message.success(menuForm.id ? '编辑成功' : '添加成功')
menuModalVisible.value = false
modalLoading.value = false
}, 1000)
}
//
const handleMenuModalCancel = () => {
menuModalVisible.value = false
}
//
const handleDelete = (record: any) => {
//
message.success('删除成功')
}
</script>
<style scoped>
.menu-management {
padding: 24px;
background: #fff;
}
.table-operations {
margin-bottom: 16px;
}
.danger-link {
color: #ff4d4f;
}
.danger-link:hover {
color: #ff7875;
}
</style>

View File

@ -0,0 +1,390 @@
<template>
<div class="role-management">
<div class="table-operations">
<a-space>
<a-button type="primary" @click="showAddRoleModal">
<template #icon><plus-outlined /></template>
新增角色
</a-button>
<a-button @click="handleBatchDelete" :disabled="!selectedRowKeys.length">
<template #icon><delete-outlined /></template>
批量删除
</a-button>
</a-space>
</div>
<a-table
:columns="columns"
:data-source="roles"
:row-selection="{ selectedRowKeys, onChange: onSelectChange }"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="record.status === 1 ? 'success' : 'error'">
{{ record.status === 1 ? '启用' : '禁用' }}
</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a @click="showEditRoleModal(record)">编辑</a>
<a-divider type="vertical" />
<a @click="showPermissionModal(record)">权限</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此角色吗?"
ok-text="确定"
cancel-text="取消"
@confirm="handleDelete(record)"
>
<a class="danger-link">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<!-- 角色表单模态框 -->
<a-modal
v-model:visible="roleModalVisible"
:title="modalTitle"
@ok="handleRoleModalOk"
@cancel="handleRoleModalCancel"
:confirmLoading="modalLoading"
>
<a-form
ref="roleForm"
:model="roleForm"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="角色名称" name="name">
<a-input v-model:value="roleForm.name" placeholder="请输入角色名称" />
</a-form-item>
<a-form-item label="角色编码" name="code">
<a-input v-model:value="roleForm.code" placeholder="请输入角色编码" />
</a-form-item>
<a-form-item label="排序" name="sort">
<a-input-number v-model:value="roleForm.sort" :min="0" style="width: 100%" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-switch v-model:checked="roleForm.status" />
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea v-model:value="roleForm.remark" placeholder="请输入备注" :rows="4" />
</a-form-item>
</a-form>
</a-modal>
<!-- 权限分配模态框 -->
<a-modal
v-model:visible="permissionModalVisible"
title="分配权限"
@ok="handlePermissionModalOk"
@cancel="handlePermissionModalCancel"
:confirmLoading="permissionLoading"
width="600px"
>
<div v-if="currentRole">
<p>正在为角色 <strong>{{ currentRole.name }}</strong> 分配权限</p>
<a-tree
v-model:checkedKeys="checkedKeys"
:treeData="permissionTree"
checkable
:defaultExpandAll="true"
/>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue'
//
const columns = [
{
title: '角色名称',
dataIndex: 'name',
key: 'name',
},
{
title: '角色编码',
dataIndex: 'code',
key: 'code',
},
{
title: '排序',
dataIndex: 'sort',
key: 'sort',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
},
{
title: '操作',
key: 'action',
width: 200,
},
]
//
const roles = ref([
{
id: 1,
name: '超级管理员',
code: 'ADMIN',
sort: 1,
status: 1,
remark: '系统最高权限',
createTime: '2023-01-01 12:00:00',
},
{
id: 2,
name: '普通用户',
code: 'USER',
sort: 2,
status: 1,
remark: '普通用户权限',
createTime: '2023-01-02 12:00:00',
},
{
id: 3,
name: '访客',
code: 'GUEST',
sort: 3,
status: 1,
remark: '访客权限',
createTime: '2023-01-03 12:00:00',
},
])
//
const loading = ref(false)
//
const pagination = reactive({
current: 1,
pageSize: 10,
total: 100,
})
//
const selectedRowKeys = ref<number[]>([])
//
const roleModalVisible = ref(false)
const modalLoading = ref(false)
const modalTitle = ref('新增角色')
//
const permissionModalVisible = ref(false)
const permissionLoading = ref(false)
const currentRole = ref<any>(null)
const checkedKeys = ref<string[]>([])
//
const roleForm = reactive({
id: undefined,
name: '',
code: '',
sort: 0,
status: true,
remark: '',
})
//
const rules = {
name: [{ required: true, message: '请输入角色名称' }],
code: [{ required: true, message: '请输入角色编码' }],
sort: [{ required: true, message: '请输入排序' }],
}
//
const permissionTree = [
{
title: '系统管理',
key: 'system',
children: [
{
title: '用户管理',
key: 'user',
children: [
{ title: '查看用户', key: 'user:view' },
{ title: '新增用户', key: 'user:add' },
{ title: '编辑用户', key: 'user:edit' },
{ title: '删除用户', key: 'user:delete' },
],
},
{
title: '角色管理',
key: 'role',
children: [
{ title: '查看角色', key: 'role:view' },
{ title: '新增角色', key: 'role:add' },
{ title: '编辑角色', key: 'role:edit' },
{ title: '删除角色', key: 'role:delete' },
],
},
{
title: '菜单管理',
key: 'menu',
children: [
{ title: '查看菜单', key: 'menu:view' },
{ title: '新增菜单', key: 'menu:add' },
{ title: '编辑菜单', key: 'menu:edit' },
{ title: '删除菜单', key: 'menu:delete' },
],
},
],
},
{
title: '内容管理',
key: 'content',
children: [
{
title: '文章管理',
key: 'article',
children: [
{ title: '查看文章', key: 'article:view' },
{ title: '新增文章', key: 'article:add' },
{ title: '编辑文章', key: 'article:edit' },
{ title: '删除文章', key: 'article:delete' },
],
},
],
},
]
//
const onSelectChange = (keys: number[]) => {
selectedRowKeys.value = keys
}
//
const handleTableChange = (pag: any) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
//
}
//
const showAddRoleModal = () => {
modalTitle.value = '新增角色'
Object.assign(roleForm, {
id: undefined,
name: '',
code: '',
sort: 0,
status: true,
remark: '',
})
roleModalVisible.value = true
}
//
const showEditRoleModal = (record: any) => {
modalTitle.value = '编辑角色'
Object.assign(roleForm, {
...record,
status: record.status === 1,
})
roleModalVisible.value = true
}
//
const showPermissionModal = (record: any) => {
currentRole.value = record
//
if (record.id === 1) {
//
checkedKeys.value = ['user:view', 'user:add', 'user:edit', 'user:delete',
'role:view', 'role:add', 'role:edit', 'role:delete',
'menu:view', 'menu:add', 'menu:edit', 'menu:delete',
'article:view', 'article:add', 'article:edit', 'article:delete']
} else if (record.id === 2) {
//
checkedKeys.value = ['user:view', 'role:view', 'menu:view', 'article:view', 'article:add', 'article:edit']
} else {
// 访
checkedKeys.value = ['user:view', 'role:view', 'menu:view', 'article:view']
}
permissionModalVisible.value = true
}
//
const handleRoleModalOk = () => {
modalLoading.value = true
//
setTimeout(() => {
message.success(roleForm.id ? '编辑成功' : '添加成功')
roleModalVisible.value = false
modalLoading.value = false
}, 1000)
}
//
const handleRoleModalCancel = () => {
roleModalVisible.value = false
}
//
const handlePermissionModalOk = () => {
permissionLoading.value = true
//
setTimeout(() => {
message.success('权限分配成功')
permissionModalVisible.value = false
permissionLoading.value = false
}, 1000)
}
//
const handlePermissionModalCancel = () => {
permissionModalVisible.value = false
}
//
const handleDelete = (record: any) => {
//
message.success('删除成功')
}
//
const handleBatchDelete = () => {
//
message.success(`删除了 ${selectedRowKeys.value.length} 条记录`)
selectedRowKeys.value = []
}
</script>
<style scoped>
.role-management {
padding: 24px;
background: #fff;
}
.table-operations {
margin-bottom: 16px;
}
.danger-link {
color: #ff4d4f;
}
.danger-link:hover {
color: #ff7875;
}
</style>

View File

@ -0,0 +1,282 @@
<template>
<div class="user-management">
<div class="table-operations">
<a-space>
<a-button type="primary" @click="showAddUserModal">
<template #icon><plus-outlined /></template>
新增用户
</a-button>
<a-button @click="handleBatchDelete" :disabled="!selectedRowKeys.length">
<template #icon><delete-outlined /></template>
批量删除
</a-button>
</a-space>
</div>
<a-table
:columns="columns"
:data-source="users"
:row-selection="{ selectedRowKeys, onChange: onSelectChange }"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="record.status === 1 ? 'success' : 'error'">
{{ record.status === 1 ? '启用' : '禁用' }}
</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a @click="showEditUserModal(record)">编辑</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此用户吗?"
ok-text="确定"
cancel-text="取消"
@confirm="handleDelete(record)"
>
<a class="danger-link">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<!-- 用户表单模态框 -->
<a-modal
v-model:visible="userModalVisible"
:title="modalTitle"
@ok="handleUserModalOk"
@cancel="handleUserModalCancel"
:confirmLoading="modalLoading"
>
<a-form
ref="userForm"
:model="userForm"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="用户名" name="username">
<a-input v-model:value="userForm.username" placeholder="请输入用户名" />
</a-form-item>
<a-form-item
label="密码"
name="password"
:rules="[{ required: !userForm.id, message: '请输入密码' }]"
>
<a-input-password v-model:value="userForm.password" placeholder="请输入密码" />
</a-form-item>
<a-form-item label="昵称" name="nickname">
<a-input v-model:value="userForm.nickname" placeholder="请输入昵称" />
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value="userForm.email" placeholder="请输入邮箱" />
</a-form-item>
<a-form-item label="手机号" name="phone">
<a-input v-model:value="userForm.phone" placeholder="请输入手机号" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-switch v-model:checked="userForm.status" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue'
//
const columns = [
{
title: '用户名',
dataIndex: 'username',
key: 'username',
},
{
title: '昵称',
dataIndex: 'nickname',
key: 'nickname',
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email',
},
{
title: '手机号',
dataIndex: 'phone',
key: 'phone',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
},
{
title: '操作',
key: 'action',
width: 150,
},
]
//
const users = ref([
{
id: 1,
username: 'admin',
nickname: '管理员',
email: 'admin@example.com',
phone: '13800138000',
status: 1,
createTime: '2023-01-01 12:00:00',
},
{
id: 2,
username: 'user',
nickname: '普通用户',
email: 'user@example.com',
phone: '13800138001',
status: 1,
createTime: '2023-01-02 12:00:00',
},
])
//
const loading = ref(false)
//
const pagination = reactive({
current: 1,
pageSize: 10,
total: 100,
})
//
const selectedRowKeys = ref<number[]>([])
//
const userModalVisible = ref(false)
const modalLoading = ref(false)
const modalTitle = ref('新增用户')
//
const userForm = reactive({
id: undefined,
username: '',
password: '',
nickname: '',
email: '',
phone: '',
status: true,
})
//
const rules = {
username: [{ required: true, message: '请输入用户名' }],
nickname: [{ required: true, message: '请输入昵称' }],
email: [
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '请输入正确的邮箱格式' },
],
phone: [
{ required: true, message: '请输入手机号' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式' },
],
}
//
const onSelectChange = (keys: number[]) => {
selectedRowKeys.value = keys
}
//
const handleTableChange = (pag: any) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
//
}
//
const showAddUserModal = () => {
modalTitle.value = '新增用户'
Object.assign(userForm, {
id: undefined,
username: '',
password: '',
nickname: '',
email: '',
phone: '',
status: true,
})
userModalVisible.value = true
}
//
const showEditUserModal = (record: any) => {
modalTitle.value = '编辑用户'
Object.assign(userForm, {
...record,
password: '', //
})
userModalVisible.value = true
}
//
const handleUserModalOk = () => {
modalLoading.value = true
//
setTimeout(() => {
message.success(userForm.id ? '编辑成功' : '添加成功')
userModalVisible.value = false
modalLoading.value = false
}, 1000)
}
//
const handleUserModalCancel = () => {
userModalVisible.value = false
}
//
const handleDelete = (record: any) => {
//
message.success('删除成功')
}
//
const handleBatchDelete = () => {
//
message.success(`删除了 ${selectedRowKeys.value.length} 条记录`)
selectedRowKeys.value = []
}
</script>
<style scoped>
.user-management {
padding: 24px;
background: #fff;
}
.table-operations {
margin-bottom: 16px;
}
.danger-link {
color: #ff4d4f;
}
.danger-link:hover {
color: #ff7875;
}
</style>