Compare commits

..

1 Commits
main ... czq

Author SHA1 Message Date
2991692032 b9b9fd2f46 init
1 week ago

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Front.iml" filepath="$PROJECT_DIR$/.idea/Front.iml" />
</modules>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

@ -1,10 +1,12 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/images/默认头像.jpg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title> <meta name="description" content="UniLife学生论坛 - 专属于大学生的交流平台" />
<meta name="keywords" content="学生论坛,大学生,交流,UniLife" />
<title>UniLife学生论坛</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

File diff suppressed because it is too large Load Diff

@ -13,6 +13,7 @@
"@vue/shared": "^3.5.13", "@vue/shared": "^3.5.13",
"axios": "^1.8.3", "axios": "^1.8.3",
"element-plus": "^2.9.7", "element-plus": "^2.9.7",
"pinia": "^3.0.2",
"vee-validate": "^4.15.0", "vee-validate": "^4.15.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
@ -21,7 +22,6 @@
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",
"sass-embedded": "^1.88.0",
"typescript": "~5.7.2", "typescript": "~5.7.2",
"vite": "^6.2.0", "vite": "^6.2.0",
"vue-tsc": "^2.2.4" "vue-tsc": "^2.2.4"

@ -35,19 +35,16 @@ importers:
devDependencies: devDependencies:
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: ^5.2.1 specifier: ^5.2.1
version: 5.2.1(vite@6.2.2(sass-embedded@1.88.0))(vue@3.5.13(typescript@5.7.3)) version: 5.2.1(vite@6.2.2)(vue@3.5.13(typescript@5.7.3))
'@vue/tsconfig': '@vue/tsconfig':
specifier: ^0.7.0 specifier: ^0.7.0
version: 0.7.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)) version: 0.7.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))
sass-embedded:
specifier: ^1.88.0
version: 1.88.0
typescript: typescript:
specifier: ~5.7.2 specifier: ~5.7.2
version: 5.7.3 version: 5.7.3
vite: vite:
specifier: ^6.2.0 specifier: ^6.2.0
version: 6.2.2(sass-embedded@1.88.0) version: 6.2.2
vue-tsc: vue-tsc:
specifier: ^2.2.4 specifier: ^2.2.4
version: 2.2.8(typescript@5.7.3) version: 2.2.8(typescript@5.7.3)
@ -71,9 +68,6 @@ packages:
resolution: {integrity: sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==} resolution: {integrity: sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@bufbuild/protobuf@2.4.0':
resolution: {integrity: sha512-RN9M76x7N11QRihKovEglEjjVCQEA9PRBVnDgk9xw8JHLrcUrp4FpAVSPSH91cNbcTft3u2vpLN4GMbiKY9PJw==}
'@ctrl/tinycolor@3.6.1': '@ctrl/tinycolor@3.6.1':
resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -464,16 +458,10 @@ packages:
brace-expansion@2.0.1: brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
buffer-builder@0.2.0:
resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==}
call-bind-apply-helpers@1.0.2: call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
colorjs.io@0.5.2:
resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==}
combined-stream@1.0.8: combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -568,10 +556,6 @@ packages:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
has-symbols@1.1.0: has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -591,9 +575,6 @@ packages:
hookable@5.5.3: hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
immutable@5.1.2:
resolution: {integrity: sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==}
is-what@4.1.16: is-what@4.1.16:
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
engines: {node: '>=12.13'} engines: {node: '>=12.13'}
@ -674,134 +655,6 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
rxjs@7.8.2:
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
sass-embedded-android-arm64@1.88.0:
resolution: {integrity: sha512-YVdxVywlbXH74uomIcRsYLHF1644V+0per6YrfZndWicjfYnWqgbGq1xixdOzLxe3vac90RlsRNxTEb0VWlhmA==}
engines: {node: '>=14.0.0'}
cpu: [arm64]
os: [android]
sass-embedded-android-arm@1.88.0:
resolution: {integrity: sha512-jveGkHhHxJ2+GnNxl3OyhZAxR8YXJCSuj7JYzoVuFTxlsaFqFQwtUrvZro61xOVOrwfe8xMk2HE3ZEw6dolhBA==}
engines: {node: '>=14.0.0'}
cpu: [arm]
os: [android]
sass-embedded-android-ia32@1.88.0:
resolution: {integrity: sha512-6C4o+lGFsYcUPGtCvOdFhFLQl1rrcBUNuC4DILDayI4bZeh3Y2CjonzCT4VNKPsOm7LFGf0OKQAZm+3/oXVIKg==}
engines: {node: '>=14.0.0'}
cpu: [ia32]
os: [android]
sass-embedded-android-riscv64@1.88.0:
resolution: {integrity: sha512-zW1NmFHwPkBBg8wqVu8e5uCKeuTSk8vasB5BBEPvQubj4tWbgxrXGIVrQyseeGXJJQYSzjNiq3ua4qNoadBWJA==}
engines: {node: '>=14.0.0'}
cpu: [riscv64]
os: [android]
sass-embedded-android-x64@1.88.0:
resolution: {integrity: sha512-b33Ja8sU67CcWCX9C3M+k8AcWXOb9uhyUJuKg/2hb/RhKUqBRCpMtQhsChpV7/DyXvyevLeosy28j673qNfnuQ==}
engines: {node: '>=14.0.0'}
cpu: [x64]
os: [android]
sass-embedded-darwin-arm64@1.88.0:
resolution: {integrity: sha512-Zu+A4OzoFtZwTlcXn66ovZRTI9Ia610KJbtJBrpsXPfqR9QcCg7pPDB/zlPK5E5xFjsxGWaL0tICOifim1HCMg==}
engines: {node: '>=14.0.0'}
cpu: [arm64]
os: [darwin]
sass-embedded-darwin-x64@1.88.0:
resolution: {integrity: sha512-nZ+/j5Z4llLejNyFcLUWJvbU3WNJDKiyZ7W+Hpn/52dDhzHiNWRVHH7humfzCEgLXZctPZlr56ubaNk/RsoSlA==}
engines: {node: '>=14.0.0'}
cpu: [x64]
os: [darwin]
sass-embedded-linux-arm64@1.88.0:
resolution: {integrity: sha512-aphDl0Z4Y+YpPAqT0fEDDxZfrTXS/v36IRpGpVcbuRIua/iHd9L3wrZuwco1nbbY+sShFNiXPE1A9/k/ZGt8rw==}
engines: {node: '>=14.0.0'}
cpu: [arm64]
os: [linux]
sass-embedded-linux-arm@1.88.0:
resolution: {integrity: sha512-bjiTZ4MNvArReXgwnA56mT3i+vHH3BgkLQT3qVwRv6fVTPQpYopK8D/QzQKbrVGYKgzWPYzZfksSQFC9lzM2yA==}
engines: {node: '>=14.0.0'}
cpu: [arm]
os: [linux]
sass-embedded-linux-ia32@1.88.0:
resolution: {integrity: sha512-m+pQMD14JQeMlQ/J8vQxHXwAQPAcfLG034BQz05a8ahXmNrk9qJkrC7FLptDlhsJ6weldX54UvXceoSpw2VsxQ==}
engines: {node: '>=14.0.0'}
cpu: [ia32]
os: [linux]
sass-embedded-linux-musl-arm64@1.88.0:
resolution: {integrity: sha512-Wxo9qklXqw+eYFHLo+uE9r9sbK/xklMt6xPU/HXs+ikoJcGtmugE7KRyyWeSfvPTi8jZvgfkFfNDZD9elzxEFA==}
engines: {node: '>=14.0.0'}
cpu: [arm64]
os: [linux]
sass-embedded-linux-musl-arm@1.88.0:
resolution: {integrity: sha512-jGRZZYP8XOiE521Pep2u9ktx1FFkLHosjO7Dj/0pvjwUddBVT16jE40gv9pqtTynG0saD8jokqdkqJ+FM3NJzA==}
engines: {node: '>=14.0.0'}
cpu: [arm]
os: [linux]
sass-embedded-linux-musl-ia32@1.88.0:
resolution: {integrity: sha512-utdTihiPCCP5HdKqwblQQWz864c7CqSplSGQ+p06GS+0ZfnuB/SKhtwe8fd11v4+IN8S2o0HAQ5KtWmRmk3eTA==}
engines: {node: '>=14.0.0'}
cpu: [ia32]
os: [linux]
sass-embedded-linux-musl-riscv64@1.88.0:
resolution: {integrity: sha512-P8XB7QVSU8KJry4oxegzAnuFVWjbHc/JCHgF2ktq2dURVyxcaKDfQZtzbUgiPOKP/R6MZIFhXaJVJIhppcruEQ==}
engines: {node: '>=14.0.0'}
cpu: [riscv64]
os: [linux]
sass-embedded-linux-musl-x64@1.88.0:
resolution: {integrity: sha512-OGEfD6AAm68vZTazFkIN7Dsu0ZQY983GZU+mWE9zZPLTIBzvNrrEZrEE/mpM6LemkwbqR+GaFP6rxGrkDz0Mhw==}
engines: {node: '>=14.0.0'}
cpu: [x64]
os: [linux]
sass-embedded-linux-riscv64@1.88.0:
resolution: {integrity: sha512-3hBlfq4bXx0RkkNxvw/FPZSmUC1GMU8NE1Ef+2dJowxAeneRotHy5WXZIMKvH7NGpskf7U8ButK05U3OxPzrTA==}
engines: {node: '>=14.0.0'}
cpu: [riscv64]
os: [linux]
sass-embedded-linux-x64@1.88.0:
resolution: {integrity: sha512-FzM5mCxkFE20efDDSPO5N5o0ZKPqs51zowt2JAe5tdAzmy/jUQ0t515tph40dV2mfX0flBJgoou76gZKhylHGg==}
engines: {node: '>=14.0.0'}
cpu: [x64]
os: [linux]
sass-embedded-win32-arm64@1.88.0:
resolution: {integrity: sha512-Zp3yNEzk/gCCBIClQx8ihAGZ1YqPbjWjTnLWtruS9FcVrkrSAIjhqaesoN1Hy61aaIoiRektOyeffHH54jiQ3g==}
engines: {node: '>=14.0.0'}
cpu: [arm64]
os: [win32]
sass-embedded-win32-ia32@1.88.0:
resolution: {integrity: sha512-yUmD6BLb01ngw/gy+FcTdsCMFaoONGFYJcy6FhMr2OOcCHNjPVD+HqTF4ZRsLNbwna8PlP6XxHFzjPKzVw18xw==}
engines: {node: '>=14.0.0'}
cpu: [ia32]
os: [win32]
sass-embedded-win32-x64@1.88.0:
resolution: {integrity: sha512-j4pOP/S9vD4enRqbfwno07Xx+j0RkfVYGV31ZxzAIF+a1+3dDBlsbwgDNP68XemJx5SjpP8yM8the6nHAnMUiQ==}
engines: {node: '>=14.0.0'}
cpu: [x64]
os: [win32]
sass-embedded@1.88.0:
resolution: {integrity: sha512-GQUxgZFuej3NZ1TSPUHU8aebtYdnIeXqYsbNEEKBtE+SC7/Gr18KH1ijTAZHPw25OUfQCdtJaRy6Fo866dHmgw==}
engines: {node: '>=16.0.0'}
hasBin: true
source-map-js@1.2.1: source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -814,27 +667,12 @@ packages:
resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}
engines: {node: '>=16'} engines: {node: '>=16'}
supports-color@8.1.1:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'}
sync-child-process@1.0.2:
resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==}
engines: {node: '>=16.0.0'}
sync-message-port@1.1.3:
resolution: {integrity: sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==}
engines: {node: '>=16.0.0'}
tiny-case@1.0.3: tiny-case@1.0.3:
resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
toposort@2.0.2: toposort@2.0.2:
resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
type-fest@2.19.0: type-fest@2.19.0:
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
engines: {node: '>=12.20'} engines: {node: '>=12.20'}
@ -848,9 +686,6 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
varint@6.0.0:
resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==}
vee-validate@4.15.0: vee-validate@4.15.0:
resolution: {integrity: sha512-PGJh1QCFwCBjbHu5aN6vB8macYVWrajbDvgo1Y/8fz9n/RVIkLmZCJDpUgu7+mUmCOPMxeyq7vXUOhbwAqdXcA==} resolution: {integrity: sha512-PGJh1QCFwCBjbHu5aN6vB8macYVWrajbDvgo1Y/8fz9n/RVIkLmZCJDpUgu7+mUmCOPMxeyq7vXUOhbwAqdXcA==}
peerDependencies: peerDependencies:
@ -947,8 +782,6 @@ snapshots:
'@babel/helper-string-parser': 7.25.9 '@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9 '@babel/helper-validator-identifier': 7.25.9
'@bufbuild/protobuf@2.4.0': {}
'@ctrl/tinycolor@3.6.1': {} '@ctrl/tinycolor@3.6.1': {}
'@element-plus/icons-vue@2.3.1(vue@3.5.13(typescript@5.7.3))': '@element-plus/icons-vue@2.3.1(vue@3.5.13(typescript@5.7.3))':
@ -1112,9 +945,9 @@ snapshots:
'@types/web-bluetooth@0.0.16': {} '@types/web-bluetooth@0.0.16': {}
'@vitejs/plugin-vue@5.2.1(vite@6.2.2(sass-embedded@1.88.0))(vue@3.5.13(typescript@5.7.3))': '@vitejs/plugin-vue@5.2.1(vite@6.2.2)(vue@3.5.13(typescript@5.7.3))':
dependencies: dependencies:
vite: 6.2.2(sass-embedded@1.88.0) vite: 6.2.2
vue: 3.5.13(typescript@5.7.3) vue: 3.5.13(typescript@5.7.3)
'@volar/language-core@2.4.12': '@volar/language-core@2.4.12':
@ -1267,15 +1100,11 @@ snapshots:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
buffer-builder@0.2.0: {}
call-bind-apply-helpers@1.0.2: call-bind-apply-helpers@1.0.2:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
function-bind: 1.1.2 function-bind: 1.1.2
colorjs.io@0.5.2: {}
combined-stream@1.0.8: combined-stream@1.0.8:
dependencies: dependencies:
delayed-stream: 1.0.0 delayed-stream: 1.0.0
@ -1402,8 +1231,6 @@ snapshots:
gopd@1.2.0: {} gopd@1.2.0: {}
has-flag@4.0.0: {}
has-symbols@1.1.0: {} has-symbols@1.1.0: {}
has-tostringtag@1.0.2: has-tostringtag@1.0.2:
@ -1418,8 +1245,6 @@ snapshots:
hookable@5.5.3: {} hookable@5.5.3: {}
immutable@5.1.2: {}
is-what@4.1.16: {} is-what@4.1.16: {}
lodash-es@4.17.21: {} lodash-es@4.17.21: {}
@ -1501,102 +1326,6 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.35.0 '@rollup/rollup-win32-x64-msvc': 4.35.0
fsevents: 2.3.3 fsevents: 2.3.3
rxjs@7.8.2:
dependencies:
tslib: 2.8.1
sass-embedded-android-arm64@1.88.0:
optional: true
sass-embedded-android-arm@1.88.0:
optional: true
sass-embedded-android-ia32@1.88.0:
optional: true
sass-embedded-android-riscv64@1.88.0:
optional: true
sass-embedded-android-x64@1.88.0:
optional: true
sass-embedded-darwin-arm64@1.88.0:
optional: true
sass-embedded-darwin-x64@1.88.0:
optional: true
sass-embedded-linux-arm64@1.88.0:
optional: true
sass-embedded-linux-arm@1.88.0:
optional: true
sass-embedded-linux-ia32@1.88.0:
optional: true
sass-embedded-linux-musl-arm64@1.88.0:
optional: true
sass-embedded-linux-musl-arm@1.88.0:
optional: true
sass-embedded-linux-musl-ia32@1.88.0:
optional: true
sass-embedded-linux-musl-riscv64@1.88.0:
optional: true
sass-embedded-linux-musl-x64@1.88.0:
optional: true
sass-embedded-linux-riscv64@1.88.0:
optional: true
sass-embedded-linux-x64@1.88.0:
optional: true
sass-embedded-win32-arm64@1.88.0:
optional: true
sass-embedded-win32-ia32@1.88.0:
optional: true
sass-embedded-win32-x64@1.88.0:
optional: true
sass-embedded@1.88.0:
dependencies:
'@bufbuild/protobuf': 2.4.0
buffer-builder: 0.2.0
colorjs.io: 0.5.2
immutable: 5.1.2
rxjs: 7.8.2
supports-color: 8.1.1
sync-child-process: 1.0.2
varint: 6.0.0
optionalDependencies:
sass-embedded-android-arm: 1.88.0
sass-embedded-android-arm64: 1.88.0
sass-embedded-android-ia32: 1.88.0
sass-embedded-android-riscv64: 1.88.0
sass-embedded-android-x64: 1.88.0
sass-embedded-darwin-arm64: 1.88.0
sass-embedded-darwin-x64: 1.88.0
sass-embedded-linux-arm: 1.88.0
sass-embedded-linux-arm64: 1.88.0
sass-embedded-linux-ia32: 1.88.0
sass-embedded-linux-musl-arm: 1.88.0
sass-embedded-linux-musl-arm64: 1.88.0
sass-embedded-linux-musl-ia32: 1.88.0
sass-embedded-linux-musl-riscv64: 1.88.0
sass-embedded-linux-musl-x64: 1.88.0
sass-embedded-linux-riscv64: 1.88.0
sass-embedded-linux-x64: 1.88.0
sass-embedded-win32-arm64: 1.88.0
sass-embedded-win32-ia32: 1.88.0
sass-embedded-win32-x64: 1.88.0
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
speakingurl@14.0.1: {} speakingurl@14.0.1: {}
@ -1605,44 +1334,29 @@ snapshots:
dependencies: dependencies:
copy-anything: 3.0.5 copy-anything: 3.0.5
supports-color@8.1.1:
dependencies:
has-flag: 4.0.0
sync-child-process@1.0.2:
dependencies:
sync-message-port: 1.1.3
sync-message-port@1.1.3: {}
tiny-case@1.0.3: {} tiny-case@1.0.3: {}
toposort@2.0.2: {} toposort@2.0.2: {}
tslib@2.8.1: {}
type-fest@2.19.0: {} type-fest@2.19.0: {}
type-fest@4.37.0: {} type-fest@4.37.0: {}
typescript@5.7.3: {} typescript@5.7.3: {}
varint@6.0.0: {}
vee-validate@4.15.0(vue@3.5.13(typescript@5.7.3)): vee-validate@4.15.0(vue@3.5.13(typescript@5.7.3)):
dependencies: dependencies:
'@vue/devtools-api': 7.7.2 '@vue/devtools-api': 7.7.2
type-fest: 4.37.0 type-fest: 4.37.0
vue: 3.5.13(typescript@5.7.3) vue: 3.5.13(typescript@5.7.3)
vite@6.2.2(sass-embedded@1.88.0): vite@6.2.2:
dependencies: dependencies:
esbuild: 0.25.1 esbuild: 0.25.1
postcss: 8.5.3 postcss: 8.5.3
rollup: 4.35.0 rollup: 4.35.0
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
sass-embedded: 1.88.0
vscode-uri@3.1.0: {} vscode-uri@3.1.0: {}

@ -1,23 +1,34 @@
<template>
<HeaderBar/>
<router-view/>
</template>
<script setup lang="ts"> <script setup lang="ts">
import HeaderBar from './components/HeaderBar.vue' import { onMounted } from 'vue';
import { useUserStore, useUIStore } from './stores';
import GlobalLoading from './components/GlobalLoading.vue';
const userStore = useUserStore();
const uiStore = useUIStore();
onMounted(() => {
// UI
uiStore.initialize();
//
if (userStore.isLoggedIn) {
userStore.fetchUserInfo();
}
});
</script> </script>
<style> <template>
<div id="app">
<router-view />
<GlobalLoading />
</div>
</template>
/* 设置 body 背景渐变,清除异常布局设置 */ <style>
body{ /* 全局样式已移至styles/global.css */
height: 100%; #app {
width:100%; width: 100%;
height: 100vh;
/*弹性布局,水平垂直居中*/ overflow-x: hidden;
justify-content: center;
align-items: center;
/*渐变背景*/
background: linear-gradient(200deg, #f3e7e9, #e3eeff);
} }
</style> </style>

@ -0,0 +1,5 @@
import userApi from './user';
export {
userApi
};

@ -0,0 +1,63 @@
import { get, post as httpPost, put, del } from './request';
// 类型定义
export interface PostItem {
id: number;
title: string;
content: string;
userId: number;
nickname: string;
avatar: string;
categoryId: number;
categoryName: string;
viewCount: number;
likeCount: number;
commentCount: number;
createdAt: string;
updatedAt: string;
}
export interface CreatePostParams {
title: string;
content: string;
categoryId: number;
}
// API方法
export default {
// 获取帖子列表
getPosts(params: { page?: number; size?: number; category?: number; sort?: string }) {
return get<{ code: number; data: { total: number; list: PostItem[]; pages: number } }>('/posts', params);
},
// 获取帖子详情
getPostDetail(id: number) {
return get<{ code: number; data: PostItem }>(`/posts/${id}`);
},
// 创建帖子
createPost(data: CreatePostParams) {
return httpPost<{ code: number; data: { postId: number } }>('/posts', data);
},
// 更新帖子
updatePost(id: number, data: CreatePostParams) {
return put<{ code: number; message: string }>(`/posts/${id}`, data);
},
// 删除帖子
deletePost(id: number) {
return del<{ code: number; message: string }>(`/posts/${id}`);
},
// 点赞/取消点赞帖子
likePost(id: number) {
return httpPost<{ code: number; message: string }>(`/posts/${id}/like`);
},
// 获取用户的帖子列表
getUserPosts(userId?: number) {
const params = userId ? { userId } : {};
return get<{ code: number; data: { total: number; list: PostItem[]; pages: number } }>('/posts', params);
}
};

@ -0,0 +1,90 @@
import axios from 'axios';
import type { AxiosResponse } from 'axios';
import { ElMessage } from 'element-plus';
// 创建axios实例
const service = axios.create({
baseURL: 'http://localhost:8087',
timeout: 10000
});
// 请求拦截器
service.interceptors.request.use(
(config) => {
// 从localStorage获取token
const token = localStorage.getItem('token');
// 如果存在token则添加到请求头
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
console.log("请求已附加token");
}
return config;
},
(error) => {
console.error('请求错误:', error);
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
const res = response.data;
// 如果接口返回的状态码不是200则判断为错误
if (res.code !== 200) {
ElMessage({
message: res.message || '请求失败',
type: 'error',
duration: 5 * 1000
});
// 处理特定错误码
if (res.code === 401) {
// 未授权清除token并重定向到登录页
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(new Error(res.message || '请求失败'));
} else {
return res;
}
},
(error) => {
console.error('响应错误:', error);
// 显示错误信息
ElMessage({
message: error.message || '请求失败',
type: 'error',
duration: 5 * 1000
});
return Promise.reject(error);
}
);
// 封装GET请求
export function get<T>(url: string, params?: any): Promise<T> {
return service.get(url, { params });
}
// 封装POST请求
export function post<T>(url: string, data?: any): Promise<T> {
return service.post(url, data);
}
// 封装PUT请求
export function put<T>(url: string, data?: any): Promise<T> {
return service.put(url, data);
}
// 封装DELETE请求
export function del<T>(url: string, params?: any): Promise<T> {
return service.delete(url, { params });
}
export default service;

@ -0,0 +1,119 @@
import { get, post, put } from './request';
// 用户接口类型定义
export interface UserInfo {
username: string;
email: string;
avatar?: string;
gender?: number;
bio?: string;
birthday?: string;
studentId?: string;
department?: string;
major?: string;
grade?: string;
}
export interface LoginParams {
email: string;
password: string;
}
export interface RegisterParams {
email: string;
password: string;
username?: string;
nickname?: string;
studentId?: string;
department?: string;
major?: string;
grade?: string;
code: string;
}
export interface EmailCodeParams {
email: string;
}
export interface VerifyCodeParams {
email: string;
code: string;
}
export interface UpdateProfileParams {
username?: string;
bio?: string;
gender?: number;
birthday?: string;
}
export interface UpdatePasswordParams {
code: string;
newPassword: string;
}
// 用户API
export default {
// 登录
login(data: LoginParams) {
return post<{code: number; data: {token: string}}>(
'/users/login',
data
);
},
// 注册
register(data: RegisterParams) {
return post<{code: number; data: {token: string}}>(
'/users/register',
data
);
},
// 获取邮箱验证码
getEmailCode(data: EmailCodeParams) {
return post<{code: number; message: string}>(
'/users/code',
data
);
},
// 验证邮箱验证码
verifyEmailCode(data: VerifyCodeParams) {
return post<{code: number; data: {token: string}}>(
'/users/login/code',
data
);
},
// 获取用户信息
getUserInfo() {
return get<{code: number; data: UserInfo}>(
'/users/info'
);
},
// 更新用户资料
updateProfile(data: UpdateProfileParams) {
return put<{code: number; message: string}>(
'/users/profile',
data
);
},
// 更新用户密码
updatePassword(data: UpdatePasswordParams) {
return put<{code: number; message: string}>(
'/users/password',
data
);
},
// 上传头像
uploadAvatar(formData: FormData) {
return post<{code: number; data: {avatarUrl: string}}>(
'/users/avatar',
formData
);
}
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

@ -1,118 +0,0 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border:none;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
cursor: pointer;
transition: border-color 0.25s;
}
/*除了LogPage以外的按钮尽量使用这里的样式*/
.btn {
outline:none;
padding: 10px 24px;
margin:10px;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: all 0.3s ease;
background-color: #fff;
}
.btn-primary {
background-color: #9370DB;
color: white;
box-shadow: 0 4px 10px rgba(147, 112, 219, 0.3);
}
.btn-primary:hover {
background-color: #8a63d2;
transform: translateY(-2px);
}
.btn-secondary {
background-color: #e6e6fa;
color: #666;
box-shadow: 0 4px 10px rgba(230, 230, 250, 0.3);
}
.btn-secondary:hover {
background-color: #dcdcdc;
transform: translateY(-2px);
}
/*信息展示在card上*/
.card {
background-color: #fff;
border-radius: 20px;
padding: 30px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}
#app {
width: 100vw;
height: 100vh;
min-height: 100vh;
min-width: 100vw;
position: relative;
display: flex;
justify-content:center;
align-items:center;
flex-direction: column;
box-sizing: border-box;
overflow:auto;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

@ -1,25 +0,0 @@
<script setup lang="ts">
const images = [
new URL('@/assets/logo-carousel/1.jpeg', import.meta.url).href,
new URL('@/assets/logo-carousel/2.png', import.meta.url).href,
new URL('@/assets/logo-carousel/3.jpg', import.meta.url).href
]
</script>
<template>
<el-carousel :interval="3000" height="300px" arrow="hover">
<el-carousel-item v-for="(img, i) in images" :key="i">
<img :src="img" class="carousel-img" />
</el-carousel-item>
</el-carousel>
</template>
<style scoped>
.carousel-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
</style>

@ -0,0 +1,76 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useUIStore } from '../stores';
const uiStore = useUIStore();
//
const isVisible = computed(() => uiStore.isLoading);
const loadingText = computed(() => uiStore.loadingText);
</script>
<template>
<transition name="fade">
<div v-if="isVisible" class="loading-overlay">
<div class="loading-container">
<div class="loading-spinner"></div>
<div class="loading-text">{{ loadingText }}</div>
</div>
</div>
</transition>
</template>
<style scoped>
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-container {
background-color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
align-items: center;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid var(--secondary-color);
border-top: 4px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
.loading-text {
color: var(--text-primary);
font-size: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

@ -1,101 +0,0 @@
<script setup>
import { House, Cloudy, User, Cpu, Message, HomeFilled, MessageBox, Calendar } from '@element-plus/icons-vue'
import {useRoute} from 'vue-router'
const route = useRoute();
</script>
<template>
<header class ="header-bar" >
<!-- 左侧图标组 -->
<div class="left-icons">
<router-link to="/unilifeHome" class="icon-btn" title="首页">
<el-icon class="icon-btn" :size="24">
<HomeFilled />
</el-icon>
</router-link>
<router-link to="/cloud" class="icon-btn" title="资料分享">
<el-icon class="icon-btn" :size="24">
<MessageBox />
</el-icon>
</router-link>
<router-link to="/self" class="icon-btn" title="日程">
<el-icon class = "icon-btn" :size="24">
<Calendar />
</el-icon>
</router-link>
<router-link to="/assistant" class="icon-btn" title="AI助手">
<el-icon class = "icon-btn" :size="24">
<Cpu />
</el-icon>
</router-link>
</div>
<!-- 右侧部分 -->
<div class="right-section">
<router-link to="/message" class="icon-btn" title="消息">
<Message size="24" />
</router-link>
<router-link to="/personal" class="user-entry" title="个人主页">
<span>个人主页</span>
</router-link>
<router-link to="/log" class="icon-btn" title="登录">
<el-icon class = "icon-btn" :size="24">
<User />
</el-icon>
</router-link>
</div>
</header>
</template>
<style scoped>
.header-bar {
height: 70px;
width: 100%;
background: #ead1fb;
position:absolute;
top: 0;
left:0;
padding:0;
margin:0;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
z-index: 10000;
}
.header-bar--personal {
background: linear-gradient(to top, #c9e4ff, #fad0c4);
}
.left-icons,
.right-section {
padding:50px;
display: flex;
align-items: center;
}
.icon-btn {
margin: 0 10px;
color: #606266;
cursor: pointer;
transition: transform 0.2s;
}
.icon-btn:hover {
transform: scale(1.1);
color: #409EFF;
}
.user-entry {
margin-left: 12px;
font-weight: 600;
color: #303133;
cursor: pointer;
}
</style>

@ -1,26 +0,0 @@
<script setup lang="ts">
defineProps<{ title: string; link: string }>()
</script>
<template>
<router-link :to="link" class="hot-topic-item">
{{ title }}
</router-link>
</template>
<style lang="scss" scoped>
.hot-topic-item {
display: block;
background-color: #fbefff;
border-radius: 8px;
padding: 10px;
margin-bottom: 8px;
color: #333;
text-decoration: none;
&:hover {
background-color: #e4d4ff;
}
}
</style>

@ -1,237 +0,0 @@
<script set lang="ts">
import { defineComponent,ref } from 'vue';
import { useRouter,useRoute } from 'vue-router';
export default defineComponent({
name: 'Personal',
setup(){
const router = useRouter();
const route = useRoute();
// `li`
const activeIndex = ref<number>(0);
// `active`
const setActive = (index: number) => {
activeIndex.value = index; // Vue `active`
};
return {
activeIndex,
router,
route,
setActive,
};
}
});
</script>
<template>
<router-view/>
<div class = "shell">
<ul class="nav">
<li :class="{active: route.name === 'Home'}" @click ="setActive(0)" id = "avatar">
<router-link :to="{name:'Home'}">
<div class="icon">
<div class="imageBox">
<img src="@/assets/images/默认头像.jpg">
</div>
</div>
<div class="text">测试样例</div>
</router-link>
</li>
<li :class="{active:route.name === 'Manager'}" @click="setActive(1)">
<router-link :to="{name:'Manager'}">
<div class="icon">
<div class="imageBox">
<img src="@/assets/images/个人.png">
</div>
</div>
<div class="text">账号管理</div>
</router-link>
</li>
<li :class="{active:route.name === 'AiManger'}" @click="setActive(2)">
<router-link :to="{name:'Manager'}">
<div class="icon">
<div class="imageBox">
<img src="@/assets/images/个人.png">
</div>
</div>
<div class="text">测试样例2</div>
</router-link>
</li>
<li :class="{active:route.name === 'AiManager'}" @click="setActive(3)">
<router-link :to="{name:'Manager'}">
<div class="icon">
<div class="imageBox">
<img src="@/assets/images/个人.png">
</div>
</div>
<div class="text">测试样例3</div>
</router-link>
</li>
<li :class="{active:route.name === 'AiManager'}" @click="setActive(4)">
<router-link :to="{name:'Manager'}">
<div class="icon">
<div class="imageBox">
<img src="@/assets/images/个人.png">
</div>
</div>
<div class="text">测试样例4</div>
</router-link>
</li>
</ul>
</div>
</template>
<style scoped>
*{
margin:0;
padding:0;
box-sizing : border-box;
list-style:none;
text-decoration:none;
}
section{
position:relative;
width:100%;
height:100vh;
display:flex;
justify-content:center;
align-items:center;
font:900 100px '';
color:rgba(175, 90, 240, 0.308);
background-color: #e4e9f5;
}
.shell{
position:fixed;
top:0%;
left:0%;
width:84px;
height:100%;
background-color:#ead1fb;
z-index:9999;
transition:width 0.5s;
padding-left:10px;
overflow:hidden;
}
.shell:hover{
width:300px;
}
.imageBox{
position:relative;
width:50px;
height:50px;
border-radius:50%;
overflow:hidden;
}
.imageBox img{
width:100%;
height:100%;
object-fit:cover;
}
.shell ul{
position:relative;
height:100vh;
}
.shell ul li{
position:relative;
padding:7px;
}
.active{
background-color: #fff;
border-top-left-radius: 50px;
border-bottom-left-radius: 50px;
}
.active::before{
content:"";
position:absolute;
top:-30px;
right:0;
width:30px;
height:30px;
border-bottom-right-radius:25px;
box-shadow:5px 5px 0 5px #fff;
background:transparent;
}
.active::after{
content:"";
position:absolute;
bottom:-30px;
right:0;
width:30px;
height:30px;
border-top-right-radius: 25px;
box-shadow:5px -5px 0 5px #fff;
background:transparent;
}
#avatar{
margin:100px 0 100px 0;
}
.shell ul li a{
position:relative;
display:flex;
white-space:nowrap;
}
.icon{
position:relative;
display:flex;
justify-content:center;
align-items: center;
min-width:60px;
padding-left:10px;
height:70px;
color:#333;
transition:0.5s;
color: rgb(153, 109, 240)
}
.icon i{
font-size:30px;
z-index:999;
}
.text{
position:relative;
height:70px;
display:flex;
align-items:center;
font-size:20px;
color:#333; /*字体颜色 */
padding-left:15px;
text-transform:uppercase;
letter-spacing:2px;/*字体间距*/
transition:0.5s;
}
.shell ul li:hover a .icon,
.shell ul li:hover a .text
{
color: #f3e7e9;/*字体和图标被选中后的颜色 */
}
.active a .icon::before{
content:"";
position:absolute;
inset:5px;
width:60px;
background:#fff;
border-radius:50%;
transition:0.5s;
border:7px solid rgb(110,90,240);
box-sizing:border-box;
}
</style>

@ -1,41 +0,0 @@
<script setup lang="ts">
defineProps<{
post: {
title: string
tags: string[]
excerpt: string
link: string
}
}>()
</script>
<template>
<router-link :to="post.link" class="post-card">
<h3>{{ post.title }}</h3>
<div class="tags">
<el-tag v-for="(tag, i) in post.tags" :key="i" type="info">{{ tag }}</el-tag>
</div>
<p>{{ post.excerpt }}</p>
</router-link>
</template>
<style lang="scss" scoped>
.post-card {
display: block;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
transition: transform 0.2s;
color: inherit;
text-decoration: none;
&:hover {
transform: translateY(-2px);
}
.tags {
margin: 8px 0;
}
}
</style>

@ -1,19 +0,0 @@
<script setup lang="ts">
</script>
<template>
<div class="schedule-placeholder">
<p>此处预留今日行程组件位置</p>
</div>
</template>
<style scoped>
.schedule-placeholder {
background: #f5f0ff;
border-radius: 8px;
padding: 16px;
min-height: 200px;
text-align: center;
color: #999;
}
</style>

@ -1,27 +0,0 @@
import request from "../../src/utils/request"
export function useEmailCode(){
const sendEmailCode = async(email:string) =>
{
return await request.post('/user/code',
{
params:{email:email}
}
)
}
const verifyEmailCode = async(email:string,code:string) =>
{
return await request.post('users/login/code',
{
params:{email:email,code:code}
}
)
}
return{
sendEmailCode,
verifyEmailCode
}
}

@ -0,0 +1,79 @@
import { ref } from 'vue';
import { userApi } from '../api';
import { ElMessage } from 'element-plus';
export function useEmailCode() {
const isSending = ref(false);
const countdown = ref(0);
let timer: number | null = null;
// 发送邮箱验证码
const sendEmailCode = async (email: string) => {
if (isSending.value) return;
if (!email) {
ElMessage.warning('请输入邮箱地址');
return;
}
try {
isSending.value = true;
const res = await userApi.getEmailCode({ email });
if (res.code === 200) {
ElMessage.success('验证码已发送,请查收邮件');
startCountdown();
}
return res;
} catch (error) {
console.error('发送验证码失败:', error);
ElMessage.error('发送验证码失败,请稍后重试');
} finally {
isSending.value = false;
}
};
// 验证邮箱验证码
const verifyEmailCode = async (email: string, code: string) => {
if (!email || !code) {
ElMessage.warning('请输入邮箱和验证码');
return;
}
try {
const res = await userApi.verifyEmailCode({ email, code });
return res;
} catch (error) {
console.error('验证码验证失败:', error);
throw error;
}
};
// 开始倒计时
const startCountdown = () => {
countdown.value = 60;
if (timer) {
clearInterval(timer);
}
timer = window.setInterval(() => {
if (countdown.value > 0) {
countdown.value--;
} else {
if (timer) {
clearInterval(timer);
timer = null;
}
}
}, 1000);
};
return {
isSending,
countdown,
sendEmailCode,
verifyEmailCode
};
}

@ -0,0 +1,27 @@
<script setup lang="ts">
import { useUserStore } from '../stores';
import { onMounted } from 'vue';
const userStore = useUserStore();
onMounted(async () => {
//
if (userStore.isLoggedIn) {
await userStore.fetchUserInfo();
}
});
</script>
<template>
<div class="base-layout">
<router-view />
</div>
</template>
<style scoped>
.base-layout {
width: 100%;
height: 100vh;
overflow-x: hidden;
}
</style>

@ -0,0 +1,263 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useUserStore } from '../stores';
const router = useRouter();
const route = useRoute();
const userStore = useUserStore();
//
const menuItems = [
{ name: 'Home', title: '个人主页', icon: 'home' },
{ name: 'AccountManager', title: '账号管理', icon: 'user' },
{ name: 'Posts', title: '我的帖子', icon: 'document' },
{ name: 'Messages', title: '消息中心', icon: 'message' },
{ name: 'Settings', title: '设置', icon: 'setting' }
];
//
const activeIndex = ref(0);
//
const setActive = (index: number) => {
activeIndex.value = index;
router.push({ name: menuItems[index].name });
};
//
onMounted(() => {
const currentRouteName = route.name as string;
const index = menuItems.findIndex(item => item.name === currentRouteName);
if (index !== -1) {
activeIndex.value = index;
}
//
if (userStore.isLoggedIn) {
userStore.fetchUserInfo();
} else {
//
router.push('/login');
}
});
// 退
const logout = () => {
userStore.logout();
router.push('/login');
};
</script>
<template>
<div class="personal-layout">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<div class="avatar">
<img :src="userStore.userInfo?.avatar || '/images/默认头像.jpg'" alt="用户头像">
</div>
<div class="username">{{ userStore.userInfo?.username || '用户' }}</div>
</div>
<ul class="menu">
<li
v-for="(item, index) in menuItems"
:key="index"
:class="{ active: activeIndex === index }"
@click="setActive(index)"
>
<div class="menu-item">
<div class="icon">
<el-icon>
<component :is="item.icon"></component>
</el-icon>
</div>
<div class="title">{{ item.title }}</div>
</div>
</li>
</ul>
<div class="sidebar-footer">
<button class="logout-btn" @click="logout">
<el-icon><switch-button /></el-icon>
<span>退出登录</span>
</button>
</div>
</div>
<!-- 主内容区 -->
<div class="main-content">
<router-view />
</div>
</div>
</template>
<style scoped>
.personal-layout {
display: flex;
width: 100%;
height: 100vh;
overflow: hidden;
}
.sidebar {
width: var(--sidebar-width);
height: 100%;
background-color: var(--secondary-color);
transition: width var(--transition-slow);
overflow: hidden;
display: flex;
flex-direction: column;
z-index: 100;
}
.sidebar:hover {
width: var(--sidebar-width-expanded);
}
.sidebar-header {
padding: var(--spacing-lg);
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: var(--spacing-xl);
}
.avatar {
width: 60px;
height: 60px;
border-radius: var(--border-radius-full);
overflow: hidden;
margin-bottom: var(--spacing-md);
border: 3px solid var(--primary-light);
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.username {
color: var(--text-primary);
font-weight: 500;
white-space: nowrap;
}
.menu {
flex: 1;
display: flex;
flex-direction: column;
padding: 0 var(--spacing-sm);
}
.menu li {
position: relative;
margin-bottom: var(--spacing-md);
cursor: pointer;
}
.menu-item {
display: flex;
align-items: center;
padding: var(--spacing-md);
border-radius: var(--border-radius-md);
transition: background-color var(--transition-normal);
}
.menu-item:hover {
background-color: rgba(255, 255, 255, 0.5);
}
.menu li.active .menu-item {
background-color: var(--primary-color);
color: white;
}
.icon {
display: flex;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
margin-right: var(--spacing-md);
}
.title {
white-space: nowrap;
}
.sidebar-footer {
padding: var(--spacing-lg);
display: flex;
justify-content: center;
}
.logout-btn {
display: flex;
align-items: center;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--border-radius-md);
background-color: rgba(255, 255, 255, 0.5);
color: var(--text-primary);
transition: background-color var(--transition-normal);
}
.logout-btn:hover {
background-color: rgba(255, 255, 255, 0.8);
}
.logout-btn .el-icon {
margin-right: var(--spacing-sm);
}
.main-content {
flex: 1;
padding: var(--spacing-xl);
overflow-y: auto;
background-color: var(--bg-secondary);
}
/* 响应式设计 */
@media (max-width: 768px) {
.personal-layout {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
flex-direction: row;
padding: var(--spacing-sm);
}
.sidebar:hover {
width: 100%;
}
.sidebar-header {
margin-bottom: 0;
padding: var(--spacing-sm);
}
.menu {
flex-direction: row;
padding: 0;
overflow-x: auto;
}
.menu li {
margin-right: var(--spacing-md);
margin-bottom: 0;
}
.sidebar-footer {
padding: var(--spacing-sm);
}
.main-content {
padding: var(--spacing-md);
}
}
</style>

@ -1,16 +1,26 @@
import { createApp } from 'vue' import { createApp } from 'vue';
import '@/assets/style/style.css' import { createPinia } from 'pinia';
import App from './App.vue' import ElementPlus from 'element-plus';
import ElementPlus from 'element-plus' import * as ElementPlusIconsVue from '@element-plus/icons-vue';
import 'element-plus/dist/index.css' import App from './App.vue';
import router from './routers/routers' import router from './router';
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App) // 样式
import 'element-plus/dist/index.css';
import './styles/global.css';
app.use(ElementPlus) // 创建应用实例
app.use(router) const app = createApp(App);
for(const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component) // 使用插件
app.use(createPinia());
app.use(ElementPlus);
app.use(router);
// 注册所有Element Plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
} }
app.mount('#app')
// 挂载应用
app.mount('#app');

@ -0,0 +1,129 @@
import { createRouter, createWebHistory } from 'vue-router';
import type { RouteRecordRaw } from 'vue-router';
import { useUserStore } from '../stores';
// 布局
import BaseLayout from '../layouts/BaseLayout.vue';
import PersonalLayout from '../layouts/PersonalLayout.vue';
// 页面
import Login from '../views/Login.vue';
import Home from '../views/Home.vue';
import AccountManager from '../views/AccountManager.vue';
import NotFound from '../views/NotFound.vue';
// 路由配置
const routes: Array<RouteRecordRaw> = [
{
path: '/',
component: BaseLayout,
children: [
{
path: '',
redirect: '/login'
},
{
path: 'login',
name: 'Login',
component: Login,
meta: {
title: '登录 - UniLife学生论坛',
requiresAuth: false
}
}
]
},
{
path: '/personal',
component: PersonalLayout,
meta: {
requiresAuth: true
},
children: [
{
path: '',
name: 'Home',
component: Home,
meta: {
title: '个人主页 - UniLife学生论坛',
requiresAuth: true
}
},
{
path: 'account',
name: 'AccountManager',
component: AccountManager,
meta: {
title: '账号管理 - UniLife学生论坛',
requiresAuth: true
}
},
// 其他个人中心页面可以在这里添加
{
path: 'posts',
name: 'Posts',
component: () => import('../views/NotFound.vue'), // 暂时使用NotFound页面
meta: {
title: '我的帖子 - UniLife学生论坛',
requiresAuth: true
}
},
{
path: 'messages',
name: 'Messages',
component: () => import('../views/NotFound.vue'), // 暂时使用NotFound页面
meta: {
title: '消息中心 - UniLife学生论坛',
requiresAuth: true
}
},
{
path: 'settings',
name: 'Settings',
component: () => import('../views/NotFound.vue'), // 暂时使用NotFound页面
meta: {
title: '设置 - UniLife学生论坛',
requiresAuth: true
}
}
]
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: NotFound,
meta: {
title: '页面不存在 - UniLife学生论坛'
}
}
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
});
// 全局前置守卫
router.beforeEach((to, from, next) => {
// 设置页面标题
document.title = to.meta.title as string || 'UniLife学生论坛';
// 检查是否需要登录权限
if (to.matched.some(record => record.meta.requiresAuth)) {
const userStore = useUserStore();
// 如果需要登录但用户未登录,重定向到登录页
if (!userStore.isLoggedIn) {
next({
path: '/login',
query: { redirect: to.fullPath }
});
} else {
next();
}
} else {
next();
}
});
export default router;

@ -1,57 +0,0 @@
import type { RouteRecord, RouteRecordRaw } from 'vue-router';
import { createWebHashHistory, createRouter,createWebHistory } from 'vue-router';
import LogPage from '../views/LogPage.vue';
import Personal from '@/components/Personal.vue';
import Manager from '@/views/AcountManager.vue';
import PersonalHome from '@/views/Home.vue';
import ForumHome from '@/views/ForumHome.vue';
const routes:Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/log',
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/404.vue'),
},
{
path:'/log',
name: 'LogPage',
component: LogPage
},
{
path:'/personal',
name: 'Personal',
component: Personal,
children: [
{
path:'',
name:'Home',
component:PersonalHome,
},
{
path:'manager',
name: 'Manager',
component:Manager,
},
{
path:'ai',
redirect: '/personal',
},
]
},
{
path:'/uniLifeHome',
name: 'ForumHome',
component: ForumHome,
}
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
});
export default router;

@ -0,0 +1,7 @@
import { useUserStore } from './user';
import { useUIStore } from './ui';
export {
useUserStore,
useUIStore
};

@ -0,0 +1,66 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useUIStore = defineStore('ui', () => {
// 状态
const isMobileView = ref(false);
const isSidebarCollapsed = ref(false);
const isDarkMode = ref(false);
const isLoading = ref(false);
const loadingText = ref('加载中...');
// 检测是否为移动视图
const checkMobileView = () => {
isMobileView.value = window.innerWidth < 768;
};
// 切换侧边栏状态
const toggleSidebar = () => {
isSidebarCollapsed.value = !isSidebarCollapsed.value;
};
// 切换暗黑模式
const toggleDarkMode = () => {
isDarkMode.value = !isDarkMode.value;
// 应用暗黑模式到文档
if (isDarkMode.value) {
document.documentElement.classList.add('dark-mode');
} else {
document.documentElement.classList.remove('dark-mode');
}
};
// 设置加载状态
const setLoading = (loading: boolean, text?: string) => {
isLoading.value = loading;
if (text) {
loadingText.value = text;
}
};
// 初始化
const initialize = () => {
// 检测移动视图
checkMobileView();
window.addEventListener('resize', checkMobileView);
// 检测系统暗黑模式偏好
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (prefersDarkMode) {
toggleDarkMode();
}
};
return {
isMobileView,
isSidebarCollapsed,
isDarkMode,
isLoading,
loadingText,
toggleSidebar,
toggleDarkMode,
setLoading,
initialize
};
});

@ -0,0 +1,187 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import userApi from '../api/user';
import type { UserInfo } from '../api/user';
import { ElMessage } from 'element-plus';
export const useUserStore = defineStore('user', () => {
// 状态
const token = ref<string>(localStorage.getItem('token') || '');
const userInfo = ref<UserInfo | null>(null);
const isLoggedIn = ref<boolean>(!!token.value);
const loading = ref<boolean>(false);
// 设置token
const setToken = (newToken: string) => {
token.value = newToken;
localStorage.setItem('token', newToken);
isLoggedIn.value = true;
};
// 清除token
const clearToken = () => {
token.value = '';
localStorage.removeItem('token');
isLoggedIn.value = false;
};
// 登录
const login = async (email: string, password: string) => {
try {
loading.value = true;
const res = await userApi.login({ email, password });
if (res.code === 200 && res.data.token) {
setToken(res.data.token);
ElMessage.success('登录成功');
return true;
}
return false;
} catch (error) {
console.error('登录失败:', error);
return false;
} finally {
loading.value = false;
}
};
// 通过验证码登录
const loginWithCode = async (email: string, code: string) => {
try {
loading.value = true;
const res = await userApi.verifyEmailCode({ email, code });
if (res.code === 200 && res.data.token) {
setToken(res.data.token);
ElMessage.success('登录成功');
return true;
}
return false;
} catch (error) {
console.error('登录失败:', error);
return false;
} finally {
loading.value = false;
}
};
// 注册
const register = async (email: string, password: string, code: string) => {
try {
loading.value = true;
const res = await userApi.register({ email, password, code });
if (res.code === 200 && res.data.token) {
setToken(res.data.token);
ElMessage.success('注册成功');
return true;
}
return false;
} catch (error) {
console.error('注册失败:', error);
return false;
} finally {
loading.value = false;
}
};
// 登出
const logout = () => {
clearToken();
userInfo.value = null;
ElMessage.success('已退出登录');
};
// 获取用户信息
const fetchUserInfo = async () => {
if (!token.value) return null;
try {
loading.value = true;
const res = await userApi.getUserInfo();
if (res.code === 200) {
userInfo.value = res.data;
return res.data;
}
return null;
} catch (error) {
console.error('获取用户信息失败:', error);
return null;
} finally {
loading.value = false;
}
};
// 更新用户资料
const updateProfile = async (data: {
username?: string;
bio?: string;
gender?: number;
birthday?: string;
}) => {
try {
loading.value = true;
const res = await userApi.updateProfile(data);
if (res.code === 200) {
// 更新本地用户信息
if (userInfo.value) {
userInfo.value = {
...userInfo.value,
...data
};
}
ElMessage.success('个人资料更新成功');
return true;
}
return false;
} catch (error) {
console.error('更新个人资料失败:', error);
return false;
} finally {
loading.value = false;
}
};
// 更新密码
const updatePassword = async (code: string, newPassword: string) => {
try {
loading.value = true;
const res = await userApi.updatePassword({ code, newPassword });
if (res.code === 200) {
ElMessage.success('密码修改成功');
return true;
}
return false;
} catch (error) {
console.error('修改密码失败:', error);
return false;
} finally {
loading.value = false;
}
};
return {
token,
userInfo,
isLoggedIn,
loading,
login,
loginWithCode,
register,
logout,
fetchUserInfo,
updateProfile,
updatePassword
};
});

@ -0,0 +1,151 @@
@import './variables.css';
@import './reset.css';
/* 全局样式 */
body {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color: var(--text-primary);
background-color: var(--bg-primary);
min-height: 100vh;
margin: 0;
padding: 0;
}
#app {
width: 100%;
height: 100vh;
margin: 0;
padding: 0;
}
/* 通用容器 */
.container {
width: 100%;
max-width: var(--content-max-width);
margin: 0 auto;
padding: var(--spacing-lg);
}
/* 卡片样式 */
.card {
background-color: var(--bg-primary);
border-radius: var(--border-radius-lg);
padding: var(--spacing-xl);
box-shadow: var(--shadow-md);
transition: transform var(--transition-normal), box-shadow var(--transition-normal);
}
.card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-lg);
}
/* 按钮样式 */
.btn {
border: none;
border-radius: var(--border-radius-md);
padding: var(--spacing-sm) var(--spacing-lg);
font-size: var(--font-size-md);
font-weight: 500;
cursor: pointer;
transition: all var(--transition-normal);
}
.btn-primary {
background-color: var(--primary-color);
color: white;
box-shadow: 0 4px 10px rgba(147, 112, 219, 0.3);
}
.btn-primary:hover {
background-color: var(--primary-dark);
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(147, 112, 219, 0.4);
}
.btn-secondary {
background-color: var(--secondary-color);
color: var(--text-secondary);
box-shadow: 0 4px 10px rgba(230, 230, 250, 0.3);
}
.btn-secondary:hover {
background-color: #dcdcdc;
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(230, 230, 250, 0.4);
}
/* 表单样式 */
.form-group {
margin-bottom: var(--spacing-md);
display: flex;
align-items: center;
}
.form-label {
width: 100px;
font-size: var(--font-size-lg);
color: var(--text-secondary);
}
.form-input {
flex: 1;
padding: var(--spacing-sm) var(--spacing-md);
border: 2px solid var(--border-color);
border-radius: var(--border-radius-md);
outline: none;
transition: border-color var(--transition-normal);
font-size: var(--font-size-md);
}
.form-input:focus {
border-color: var(--primary-light);
}
/* 响应式设计 */
@media (max-width: 1024px) {
.container {
padding: var(--spacing-md);
}
}
@media (max-width: 768px) {
.form-group {
flex-direction: column;
align-items: flex-start;
}
.form-label {
width: 100%;
margin-bottom: var(--spacing-xs);
}
}
/* 动画 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.fade-in {
animation: fadeIn var(--transition-normal);
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
.float {
animation: float 5s ease-in-out infinite;
}

@ -0,0 +1,44 @@
/* CSS Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
a {
text-decoration: none;
color: inherit;
}
ul, ol {
list-style: none;
}
button {
background: none;
border: none;
cursor: pointer;
}

@ -0,0 +1,57 @@
:root {
/* 主题颜色 */
--primary-color: #9370DB;
--primary-light: #b19cd9;
--primary-dark: #8a63d2;
--secondary-color: #e6e6fa;
/* 文本颜色 */
--text-primary: #333333;
--text-secondary: #666666;
--text-light: #999999;
/* 背景颜色 */
--bg-primary: #ffffff;
--bg-secondary: #f9f7ff;
--bg-gradient: linear-gradient(200deg, #f3e7e9, #e3eeff);
/* 边框颜色 */
--border-color: #e6e6fa;
/* 阴影 */
--shadow-sm: 0 2px 5px rgba(0, 0, 0, 0.05);
--shadow-md: 0 5px 15px rgba(0, 0, 0, 0.05);
--shadow-lg: 0 8px 20px rgba(0, 0, 0, 0.1);
/* 圆角 */
--border-radius-sm: 5px;
--border-radius-md: 10px;
--border-radius-lg: 20px;
--border-radius-full: 50%;
/* 间距 */
--spacing-xs: 5px;
--spacing-sm: 10px;
--spacing-md: 15px;
--spacing-lg: 20px;
--spacing-xl: 30px;
/* 字体大小 */
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-md: 1rem;
--font-size-lg: 1.25rem;
--font-size-xl: 1.5rem;
--font-size-xxl: 2rem;
/* 过渡 */
--transition-fast: 0.2s ease;
--transition-normal: 0.3s ease;
--transition-slow: 0.5s ease;
/* 布局 */
--sidebar-width: 84px;
--sidebar-width-expanded: 300px;
--header-height: 60px;
--content-max-width: 1280px;
}

@ -1,37 +0,0 @@
import axios from 'axios';
const service = axios.create({
baseURL: 'http://localhost:8080',
timeout: 5000
});
service.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
console.log("前端发送信息");
return config;
}
else
{ console.log("没有token");
return config;
}
},
error => {
// 对请求错误做些什么
return Promise.reject(error);
}
);
service.interceptors.response.use(
response => {
console.log("后端返回信息");
return response.data;
},
error => {
// 对响应错误做些什么
return Promise.reject(error);
}
);
export default service;

@ -1,44 +0,0 @@
<template>
<div class="not-found-container">
<el-icon class="not-found-icon"><Warning /></el-icon>
<h1>404 - 页面未找到</h1>
<p>你访问的页面不存在可能是链接失效或地址错误</p>
<button class = "btn btn-primary" @click="goHome"></button>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { Warning } from '@element-plus/icons-vue'
const router = useRouter()
const goHome = () => {
router.push('/') // '/log'
}
</script>
<style scoped>
.not-found-container {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}
.not-found-icon {
font-size: 64px;
color: #b19cd9;
margin-bottom: 20px;
}
h1 {
margin-bottom: 10px;
}
p {
margin-bottom: 30px;
}
</style>

@ -0,0 +1,733 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { useForm } from 'vee-validate';
import * as yup from 'yup';
import { ElMessage } from 'element-plus';
import { useUserStore } from '../stores';
import { useEmailCode } from '../hooks/useEmailCode';
const userStore = useUserStore();
const { sendEmailCode, countdown } = useEmailCode();
//
const isProfileEditable = ref(false);
const isPasswordEditable = ref(false);
//
const profileForm = reactive({
username: '',
gender: 0,
bio: '',
birthday: ''
});
//
const passwordForm = reactive({
newPassword: '',
confirmPassword: '',
code: ''
});
//
const avatarUrl = ref('');
const isAvatarHovered = ref(false);
const isAvatarDialogVisible = ref(false);
const avatarPreviewUrl = ref('');
const selectedAvatarFile = ref<File | null>(null);
//
const genderOptions = [
{ label: '男', value: 1 },
{ label: '女', value: 2 },
{ label: '保密', value: 0 }
];
//
const profileValidationSchema = yup.object({
username: yup.string().required('用户名不能为空'),
bio: yup.string()
});
const passwordValidationSchema = yup.object({
newPassword: yup.string().required('新密码不能为空').min(6, '密码至少6位'),
confirmPassword: yup.string()
.required('确认密码不能为空')
.oneOf([yup.ref('newPassword')], '两次密码不一致'),
code: yup.string().required('验证码不能为空')
});
const { handleSubmit: handleProfileSubmit } = useForm({
validationSchema: profileValidationSchema,
initialValues: profileForm
});
const { handleSubmit: handlePasswordSubmit } = useForm({
validationSchema: passwordValidationSchema,
initialValues: passwordForm
});
//
const fetchUserInfo = async () => {
const userInfo = await userStore.fetchUserInfo();
if (userInfo) {
profileForm.username = userInfo.username || '';
profileForm.gender = userInfo.gender || 0;
profileForm.bio = userInfo.bio || '';
profileForm.birthday = userInfo.birthday || '';
avatarUrl.value = userInfo.avatar || '/images/默认头像.jpg';
}
};
//
const toggleProfileEdit = () => {
if (isProfileEditable.value) {
//
const userInfo = userStore.userInfo;
if (userInfo) {
profileForm.username = userInfo.username || '';
profileForm.gender = userInfo.gender || 0;
profileForm.bio = userInfo.bio || '';
profileForm.birthday = userInfo.birthday || '';
}
}
isProfileEditable.value = !isProfileEditable.value;
};
//
const togglePasswordEdit = () => {
if (isPasswordEditable.value) {
//
passwordForm.newPassword = '';
passwordForm.confirmPassword = '';
passwordForm.code = '';
}
isPasswordEditable.value = !isPasswordEditable.value;
};
//
const submitProfile = handleProfileSubmit(async (values) => {
const success = await userStore.updateProfile({
username: values.username,
bio: values.bio,
gender: values.gender,
birthday: values.birthday
});
if (success) {
isProfileEditable.value = false;
}
});
//
const submitPassword = handlePasswordSubmit(async (values) => {
const success = await userStore.updatePassword(
values.code,
values.newPassword
);
if (success) {
isPasswordEditable.value = false;
passwordForm.newPassword = '';
passwordForm.confirmPassword = '';
passwordForm.code = '';
}
});
//
const handleSendCode = async () => {
if (!userStore.userInfo?.email) {
ElMessage.warning('无法获取邮箱地址');
return;
}
await sendEmailCode(userStore.userInfo.email);
};
//
const handleAvatarHover = (hovered: boolean) => {
isAvatarHovered.value = hovered;
};
const openAvatarDialog = () => {
isAvatarDialogVisible.value = true;
};
const handleAvatarChange = (event: Event) => {
const input = event.target as HTMLInputElement;
if (input.files && input.files[0]) {
const file = input.files[0];
selectedAvatarFile.value = file;
const reader = new FileReader();
reader.onload = (e) => {
avatarPreviewUrl.value = e.target?.result as string;
};
reader.readAsDataURL(file);
}
};
const uploadAvatar = async () => {
if (!selectedAvatarFile.value) {
ElMessage.warning('请先选择头像');
return;
}
try {
const formData = new FormData();
formData.append('avatar', selectedAvatarFile.value);
// API
// const res = await userApi.uploadAvatar(formData);
//
ElMessage.success('头像上传成功');
avatarUrl.value = avatarPreviewUrl.value;
isAvatarDialogVisible.value = false;
selectedAvatarFile.value = null;
avatarPreviewUrl.value = '';
} catch (error) {
console.error('上传头像失败:', error);
ElMessage.error('上传头像失败');
}
};
//
const codeButtonText = computed(() => {
return countdown.value > 0 ? `${countdown.value}秒后重新获取` : '获取验证码';
});
onMounted(() => {
fetchUserInfo();
});
</script>
<template>
<div class="account-manager">
<div class="page-header">
<h1>账号管理</h1>
<p>管理你的个人资料和账号信息</p>
</div>
<div class="account-container">
<!-- 左侧个人资料 -->
<div class="card profile-card">
<div class="card-header">
<h2>个人资料</h2>
<button
class="btn"
:class="isProfileEditable ? 'btn-secondary' : 'btn-primary'"
@click="toggleProfileEdit"
>
{{ isProfileEditable ? '取消' : '编辑' }}
</button>
</div>
<div class="avatar-container">
<div
class="avatar"
@mouseenter="handleAvatarHover(true)"
@mouseleave="handleAvatarHover(false)"
@click="openAvatarDialog"
>
<img :src="avatarUrl" alt="用户头像">
<div class="avatar-overlay" v-if="isAvatarHovered">
<el-icon><upload-filled /></el-icon>
<span>更换头像</span>
</div>
</div>
</div>
<form @submit.prevent="submitProfile">
<div class="form-group">
<label for="username">用户名</label>
<input
id="username"
type="text"
v-model="profileForm.username"
:readonly="!isProfileEditable"
class="form-input"
>
</div>
<div class="form-group">
<label>性别</label>
<div class="radio-group">
<label v-for="option in genderOptions" :key="option.value" class="radio-label">
<input
type="radio"
:value="option.value"
v-model="profileForm.gender"
:disabled="!isProfileEditable"
>
<span>{{ option.label }}</span>
</label>
</div>
</div>
<div class="form-group">
<label for="birthday">生日</label>
<input
id="birthday"
type="date"
v-model="profileForm.birthday"
:readonly="!isProfileEditable"
class="form-input"
>
</div>
<div class="form-group">
<label for="bio">个人简介</label>
<textarea
id="bio"
v-model="profileForm.bio"
:readonly="!isProfileEditable"
class="form-input textarea"
rows="4"
></textarea>
</div>
<div class="form-actions" v-if="isProfileEditable">
<button type="submit" class="btn btn-primary">保存修改</button>
</div>
</form>
</div>
<!-- 右侧账号信息 -->
<div class="card account-card">
<div class="card-header">
<h2>账号信息</h2>
</div>
<div class="form-group">
<label>邮箱</label>
<input
type="email"
:value="userStore.userInfo?.email || ''"
readonly
class="form-input"
>
<p class="form-hint">邮箱地址不可修改</p>
</div>
<div class="card-header password-header">
<h3>密码设置</h3>
<button
class="btn"
:class="isPasswordEditable ? 'btn-secondary' : 'btn-primary'"
@click="togglePasswordEdit"
>
{{ isPasswordEditable ? '取消' : '修改密码' }}
</button>
</div>
<form v-if="isPasswordEditable" @submit.prevent="submitPassword">
<div class="form-group">
<label for="newPassword">新密码</label>
<input
id="newPassword"
type="password"
v-model="passwordForm.newPassword"
class="form-input"
placeholder="请输入新密码"
>
</div>
<div class="form-group">
<label for="confirmPassword">确认密码</label>
<input
id="confirmPassword"
type="password"
v-model="passwordForm.confirmPassword"
class="form-input"
placeholder="请再次输入新密码"
>
</div>
<div class="form-group">
<label for="code">验证码</label>
<div class="code-input-group">
<input
id="code"
type="text"
v-model="passwordForm.code"
class="form-input"
placeholder="请输入验证码"
>
<button
type="button"
class="code-btn"
@click="handleSendCode"
:disabled="countdown > 0"
>
{{ codeButtonText }}
</button>
</div>
<p class="form-hint">验证码将发送到您的邮箱</p>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">确认修改</button>
</div>
</form>
<div class="account-security">
<h3>账号安全</h3>
<p>最后登录时间: 2023-06-15 14:30:25</p>
<p>最后登录IP: 192.168.1.1</p>
</div>
</div>
</div>
<!-- 头像上传对话框 -->
<el-dialog
v-model="isAvatarDialogVisible"
title="更换头像"
width="400px"
>
<div class="avatar-upload-container">
<div class="avatar-preview-container">
<img
v-if="avatarPreviewUrl"
:src="avatarPreviewUrl"
class="avatar-preview"
>
<div v-else class="avatar-placeholder">
<el-icon><plus /></el-icon>
<span>选择图片</span>
</div>
</div>
<input
type="file"
accept="image/*"
@change="handleAvatarChange"
class="avatar-input"
id="avatar-input"
>
<label for="avatar-input" class="btn btn-primary">选择图片</label>
</div>
<template #footer>
<div class="dialog-footer">
<button class="btn btn-secondary" @click="isAvatarDialogVisible = false">取消</button>
<button class="btn btn-primary" @click="uploadAvatar" :disabled="!avatarPreviewUrl">上传</button>
</div>
</template>
</el-dialog>
<!-- 装饰元素 -->
<div class="decoration-element star-1"></div>
<div class="decoration-element star-2"></div>
<div class="decoration-element heart">💜</div>
<div class="decoration-element cat">🐱</div>
</div>
</template>
<style scoped>
.account-manager {
max-width: 1000px;
margin: 0 auto;
position: relative;
}
.page-header {
margin-bottom: var(--spacing-xl);
}
.page-header h1 {
font-size: var(--font-size-xxl);
color: var(--primary-color);
margin-bottom: var(--spacing-xs);
}
.page-header p {
color: var(--text-secondary);
}
.account-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-xl);
}
.card {
background-color: var(--bg-primary);
border-radius: var(--border-radius-lg);
padding: var(--spacing-xl);
box-shadow: var(--shadow-md);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
}
.card-header h2 {
font-size: var(--font-size-xl);
color: var(--primary-color);
margin: 0;
}
.card-header h3 {
font-size: var(--font-size-lg);
color: var(--text-primary);
margin: 0;
}
.password-header {
margin-top: var(--spacing-xl);
}
.avatar-container {
display: flex;
justify-content: center;
margin-bottom: var(--spacing-xl);
}
.avatar {
width: 120px;
height: 120px;
border-radius: var(--border-radius-full);
overflow: hidden;
position: relative;
cursor: pointer;
border: 3px solid var(--primary-light);
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
}
.avatar-overlay .el-icon {
font-size: 24px;
margin-bottom: var(--spacing-xs);
}
.form-group {
margin-bottom: var(--spacing-lg);
}
.form-group label {
display: block;
margin-bottom: var(--spacing-sm);
color: var(--text-secondary);
font-weight: 500;
}
.form-input {
width: 100%;
padding: var(--spacing-md);
border: 2px solid var(--border-color);
border-radius: var(--border-radius-md);
font-size: var(--font-size-md);
transition: border-color var(--transition-normal);
}
.form-input:focus {
border-color: var(--primary-light);
outline: none;
}
.form-input:read-only {
background-color: var(--secondary-color);
cursor: not-allowed;
}
.textarea {
resize: vertical;
min-height: 100px;
}
.radio-group {
display: flex;
gap: var(--spacing-lg);
}
.radio-label {
display: flex;
align-items: center;
cursor: pointer;
}
.radio-label input {
margin-right: var(--spacing-xs);
}
.form-hint {
font-size: var(--font-size-sm);
color: var(--text-light);
margin-top: var(--spacing-xs);
}
.code-input-group {
display: flex;
gap: var(--spacing-md);
}
.code-input-group .form-input {
flex: 1;
}
.code-btn {
padding: var(--spacing-sm) var(--spacing-md);
background-color: var(--primary-color);
color: white;
border: none;
border-radius: var(--border-radius-md);
cursor: pointer;
transition: background-color var(--transition-normal);
white-space: nowrap;
}
.code-btn:hover:not(:disabled) {
background-color: var(--primary-dark);
}
.code-btn:disabled {
background-color: var(--text-light);
cursor: not-allowed;
}
.form-actions {
display: flex;
justify-content: flex-end;
margin-top: var(--spacing-lg);
}
.account-security {
margin-top: var(--spacing-xl);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--border-color);
}
.account-security h3 {
font-size: var(--font-size-lg);
color: var(--text-primary);
margin-bottom: var(--spacing-md);
}
.account-security p {
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
}
.avatar-upload-container {
display: flex;
flex-direction: column;
align-items: center;
}
.avatar-preview-container {
width: 200px;
height: 200px;
border-radius: var(--border-radius-md);
overflow: hidden;
margin-bottom: var(--spacing-lg);
border: 2px dashed var(--border-color);
display: flex;
justify-content: center;
align-items: center;
}
.avatar-preview {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
display: flex;
flex-direction: column;
align-items: center;
color: var(--text-light);
}
.avatar-placeholder .el-icon {
font-size: 40px;
margin-bottom: var(--spacing-sm);
}
.avatar-input {
display: none;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-md);
margin-top: var(--spacing-lg);
}
/* 装饰元素 */
.decoration-element {
position: absolute;
z-index: -1;
font-size: 24px;
animation: float 5s ease-in-out infinite;
}
.star-1 {
top: 50px;
right: 50px;
animation-delay: 0s;
}
.star-2 {
bottom: 100px;
left: 50px;
animation-delay: 1s;
}
.heart {
top: 200px;
left: 100px;
animation-delay: 2s;
}
.cat {
bottom: 50px;
right: 100px;
animation-delay: 3s;
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.account-container {
grid-template-columns: 1fr;
}
.decoration-element {
display: none;
}
}
</style>

@ -1,882 +0,0 @@
<script set lang="ts">
import { defineComponent, ref, nextTick ,watch,onMounted} from 'vue';
import{useForm,useField,Form} from 'vee-validate';
import * as yup from 'yup';
import request from '@/utils/request';
import { useGetDerivedNamespace ,ElMessage} from 'element-plus';
import { useEmailCode } from '@/components/useEmailCode';
export default defineComponent({
name: 'Manager',
//
// usernamegenderintroductionbirthdayemail,password()
//email
//usernamegenderintroductionbirthday
//
//使struct orignData{username,gender,introduction,birthday,email,password}
//使ToolTip
setup() {
const originData = ref({
username:"测试员",
avatarUrl:'@/assets/images/默认头像.jpg',
gender:2,
introduction:'只要不出bug一切都好QAQ',
birthday:'2023-10-01',
email:"test@example.com",
password:'123456',
})
const formData = ref({...originData.value});//
//
const isEditable = ref(false);
//
const showErrors = ref(false);
const PasswordEdit = ref(false);
const newpassword = ref('');
const newpasswordConfirm = ref('');
const emailCode = ref(''); //
const toggleEdit = () => {
isEditable.value = !isEditable.value;
};
const togglePasswordEdit = () => {
PasswordEdit.value = !PasswordEdit.value;
};
//
const hover = ref(false)
const dialogVisible = ref(false);
const previewUrl = ref<string>('');
const selectedFile = ref<File | null>(null)
const handleAvatarClick = ()=>
{
dialogVisible.value = true;
}
const beforeAvatarUpload = (file:File)=>
{
const isImage = file.type.startsWith('image/');
if(!isImage){
ElMessage.error('只能上传图片文件')
}
const isLt2M = file.size / 1024 / 1024 <2
if(!isLt2M)
{
ElMessage.error('图片大小不能超过2MB')
}
if(isImage && isLt2M)
{
selectedFile.value = file;
previewUrl.value = URL.createObjectURL(file)
}
return false
}
const uploadAvatar = async() =>
{
if(!selectedFile.value) return
const formData = new FormData();
formData.append('file',selectedFile.value);
try{
const res = await request.post('/users/avatar',
{
data:{
file:formData,
}
})
if(res.data.code === 200)
{
ElMessage.success('头像上传成功')
previewUrl.value = URL.createObjectURL(selectedFile.value)
}
else
{
ElMessage.error('头像上传失败')
}
}catch(error){
console.error(error)
ElMessage.error('头像上传失败')
}
}
const submitAvatar = () =>
{
uploadAvatar();
}
//alertbug
const errorsMeg = ref('');
//
//
const ProfileScheme = useForm({
initialValues:formData.value,
validationSchema:yup.object(
{
username:yup.string().required('昵称不能为空'),
introduction:yup.string().required("自我介绍不能为空"),
}
)
})
//
const PasswordSheme = useForm({
initialValues:{
newpassword:newpassword.value,
newpasswordConfirm:newpasswordConfirm.value,
emailCode:emailCode.value
},
validationSchema:yup.object(
{
newpassword:yup.string().required('新密码不能为空').min(6,'密码至少6位'),
newpasswordConfirm:yup.string().required('确认密码不能为空').oneOf([yup.ref('newpassword')],'两次输入的密码不一致'),
emailCode:yup.string().required('验证码不能为空')
}
)
})
//
//
const onProfileSubmit = async() =>{
console.log("调用个人信息提交函数");
ProfileScheme.resetForm({values:{...formData.value},
errors:{}});
PasswordSheme.resetForm({errors:{}});
//
ProfileScheme.handleSubmit((values)=>{
console.log("表单调用成功",values);
updataUserInfo().then((res)=>
{
if(res.code == 200){
console.log("个人信息保存成功");
isEditable.value = !isEditable;
}
else
{
console.log("个人信息保存失败")
}
})
},(errors) => {
console.log("表单调用失败", errors);
formData.value = { ...originData.value }; //
}
)();
}
//
const onPasswordSubmit = async()=>
{
console.log("调用密码提交函数");
PasswordSheme.resetForm({values:{
newpassword:newpassword.value,
newpasswordConfirm:newpasswordConfirm.value,
emailCode:emailCode.value
},errors:{}});
ProfileScheme.resetForm({errors:{}});
PasswordSheme.handleSubmit((values)=>{
console.log("表单调用成功",values);
modifyPassword().then((res)=>{
if(res != 200){
ElMessage.error("验证码错误");
}
else{
console.log("修改成功");
PasswordEdit.value = false;
}
})
},(err) =>{
console.log("表单调用失败",err);
})();
}
//
//axios
//
async function getUserInfo() {
try {
const response = await request.get('/user/info');
console.log('获取用户信息成功:', response.data);
//
originData.value.avatarUrl = response.data.avatar;
originData.value.gender = response.data.gender;
originData.value.email = response.data.email;
originData.value.introduction = response.data.bio;
originData.value.password = "123456";
originData.value.username = response.data.username;
originData.value.birthday = response.data.birthday;
formData.value = { ...originData.value };
} catch (error) {
console.error('获取用户信息失败:', error);
}
}
//
async function updataUserInfo(){
try{
const res = request.put('/users/profile',{
data:{
username:formData.value.username,
bio:formData.value.introduction,
gender:formData.value.gender,
birthday:formData.value.birthday,
}
})
console.log("向后端发送数据成功:",formData.value);
return (await res).data;
}
catch(error)
{
console.log("发送数据失败",error);
}
}
//
const {sendEmailCode} = useEmailCode()
async function modifyPassword()
{
try{
const res = await request.put('/users/password',{
data:{
code:emailCode,
newPassword:newpassword
}
})
if(res.data.code == 200) console.log("新密码修改成功")
else console.log("验证码错误");
return res.data
} catch (error) {
console.error('Password modification failed:', error);
}
}
onMounted(()=>{
getUserInfo();
})
return {
originData,
formData,
isEditable,
newpassword,
newpasswordConfirm,
emailCode,
sendEmailCode,
PasswordEdit,
toggleEdit,
togglePasswordEdit,
//
ProfileScheme,
PasswordSheme,
onProfileSubmit,
onPasswordSubmit,
//
showErrors,
errorsMeg,
//
hover,
dialogVisible,
previewUrl,
selectedFile,
handleAvatarClick,
beforeAvatarUpload,
uploadAvatar,
submitAvatar
};
}
});
</script>
<template>
<!--错误信息显示的地方-->
<transition name = "fade-up">
<el-alert
class = "error-msg"
v-if="showErrors"
:title= "errorsMeg"
type="error"
effect="dark"
:closable="true"
@close="showErrors = false"
show-icon = "true"
center/>
</transition>
<div class = "container">
<div class="main-content">
<!-- 左侧信息设置区 -->
<div class="card profile-info-card">
<!-- 个人信息部分 -->
<Form :validation-schema="ProfileScheme">
<div class="form-section">
<h2 class="section-title">个人信息</h2>
<div class="form-group">
<label class="form-label">昵称</label>
<input type="text" class="form-input" v-model="formData.username" :readonly="!isEditable"/>
</div>
<div class="form-group">
<label class="form-label">个人简介</label>
<input type="text" class="form-input" v-model="formData.introduction" :readonly="!isEditable">
</div>
<div class="form-group">
<label class="form-label">性别</label>
<div class="radio-group">
<label class="radio-label">
<input type="radio" name="gender" class="radio-input" v-model = "formData.gender" :value="2" :disabled="!isEditable">
<span></span>
</label>
<label class="radio-label">
<input type="radio" name="gender" class="radio-input" v-model = "formData.gender" :value="1" :disabled="!isEditable">
<span></span>
</label>
</div>
</div>
<div class="form-group">
<label class="form-label">生日</label>
<input type="date" class="date-input" v-model = "formData.birthday" :readonly="!isEditable">
</div>
</div>
<div class="btn-save-container">
<button type = "submit" class="btn btn-primary" @click.prevent="onProfileSubmit()" :disabled = "!isEditable"> </button>
<button type = "button"class="btn btn-secondary" @click="toggleEdit">{{ isEditable ? '' : ' ' }}</button>
</div>
</Form>
<div class="divider"></div>
<!-- 账号信息部分 -->
<Form :validation-schema="PasswordSheme">
<div class="form-section">
<h2 class="section-title">账号信息</h2>
<!--展示邮箱同时提供发送验证码的按钮-->
<div class="form-group">
<label class="form-label">绑定邮箱</label>
<input type="email" class="form-input" v-model="formData.email" readonly>
<button class="btn btn-primary" v-if="PasswordEdit" @click = "sendEmailCode(formData.email)"></button>
</div>
<div class="form-group">
<label class="form-label">修改密码</label>
<div class="password-inputs" v-if = "!PasswordEdit">
<input type="password" class="form-input" v-model = "formData.password" readonly>
</div>
<div class = "password-inputs" v-if = "PasswordEdit">
<input type="password" class="form-input" v-model = "newpassword" placeholder="新密码"/>
<input type="password" class="form-input" v-model = "newpasswordConfirm" placeholder="确认新密码"/>
<input type="email-code" class="form-input" v-model="emailCode" placeholder="验证码">
</div>
<button type ="button" class="btn btn-primary btn-password" @click = "togglePasswordEdit()">
{{ PasswordEdit ? '取消' : '修改' }}
</button>
<button type = "submit" class="btn btn-secondary btn-password" v-if="PasswordEdit" @click.prevent="onPasswordSubmit()">
保存
</button>
</div>
<p class="form-hint">* 密码至少包含6个字符建议使用字母数字和符号的组合</p>
</div>
</Form>
</div>
<!-- 右侧预览区 -->
<div class="card profile-preview-card">
<div class="preview-section">
<div class="avatar-wrapper" @click = "handleAvatarClick" @mouseenter = "hover = true" @mouseleave = "hover = false">
<img :src="originData.avatarUrl" alt="用户头像" class = "avatar-image">
<div v-if = "hover" class = "avatar-hover-mask">更换头像</div>
</div>
<el-dialog
v-model = "dialogVisible"
title = "更换头像"
width="70%"
:show-close="false">
<el-upload
class="avatar-uploader"
:http-request = "uploadAvatar"
:show-file-list="false"
:before-upload = "beforeAvatarUpload"
>
<img v-if="previewUrl" :src="previewUrl" class="avatar-preview"/>
<el-icon size = 30px v-else><Plus/></el-icon>
</el-upload>
<template #footer>
<el-button class = "btn btn-secondary"@click = "dialogVisible = false;previewUrl = ''">取消</el-button>
<el-button class = "btn btn-primary" type="primary" @click="submitAvatar"></el-button>
</template>
</el-dialog>
<h3>{{ formData.username }}</h3>
<p class="preview-email">{{ formData.email }}</p>
<div class="preview-bio">
{{ formData.introduction }}
</div>
</div>
<!-- 可爱装饰元素 -->
<div class="cute-decoration star-1"></div>
<div class="cute-decoration star-2"></div>
<div class="cute-decoration heart">💜</div>
<div class="cute-decoration cat">🐱</div>
<div class="cute-decoration cake">🥰🍰</div>
</div>
</div>
</div>
</template>
<style scoped>
.error-msg{
z-index: 1000;
height:50px;
width:30%;
position: absolute;
top:8%;
left:20%;
display: flex;
transition:2s;
}
/*错误信息提示*/
.fade-up-enter-active,
.fade-up-leave-active{
transition: all 0.4s ease;
}
.fade-up-enter-from,
.fade-up-leave-to{
opacity:0;
transform:translateY(10px);
}
.fade-up-enter-to,
.fade-up-leave-from{
opacity:1;
transform:translateY(0);
}
.hidden
{
display: none;
}
/* 主容器 */
.container {
position:absolute;
background-color:transparent;
left:100px;
top:2%;
width:94%;
height: 96%;
}
/* 侧边栏 */
/* 用户头像容器 */
.avatar-container {
display: flex;
justify-content: center;
margin: 20px 0 30px;
}
.avatar-wrapper{
position:relative;
width: 200px;
height:200px;
cursor:pointer;
border-radius:50%;
overflow:hidden;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
.avatar-image{
width:100%;
height:100%;
object-fit:cover;
}
.avatar-hover-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.avatar-uploader {
display: flex;
justify-content: center;
align-items: center;
height: 150px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
}
.avatar-preview {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
}
/* 主内容区 */
.main-content {
flex: 1;
height:94%;
padding: 30px;
display: flex;
gap: 20px;
}
/* 左侧内容卡片 */
.profile-info-card {
flex: 2;
}
/* 右侧内容卡片 */
.profile-preview-card {
padding-top:90px;
flex: 1;
}
/* 分隔线 */
.divider {
height: 1px;
background-color: #e6e6fa;
margin: 25px 0;
border-radius: 1px;
}
/* 表单区 */
.form-section {
margin-bottom: 30px;
}
.section-title {
font-size: 2rem;
color: #9370DB;
margin-bottom: 20px;
font-weight: 500;
}
.form-group {
height:60px;
margin-bottom: 15px;
margin-left:10px;
display: flex;
align-items: center;
}
.form-label {
width: 95px;
font-size: 1.25rem;
color: #666;
text-align:left;
}
/* 输入框样式 */
.form-input {
flex: 1;
height:30px;
padding: 10px 15px;
border: 2px solid #e6e6fa;
border-radius: 25px;
outline: none;
transition: border-color 0.3s ease;
font-size: 1.2rem;
}
.form-input:focus {
border-color: #b19cd9;
}
/* 单选按钮样式 */
.radio-group {
transform:scale(1.2);
display: flex;
gap: 30px;
}
.radio-label {
display: flex;
align-items: center;
cursor: pointer;
}
.radio-input {
appearance: none;
-webkit-appearance: none;
width: 20px;
height: 20px;
border: 2px solid #e6e6fa;
border-radius: 50%;
margin-right: 8px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.radio-input:checked {
border-color: #9370DB;
}
.radio-input:checked::before {
content: "";
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #9370DB;
}
/* 日期选择器 */
.date-input {
width: 100%;
padding: 10px 15px;
border: 2px solid #e6e6fa;
border-radius: 25px;
outline: none;
transition: border-color 0.3s ease;
background-color: #fff;
font-size:1.2rem;
}
.date-input:focus {
border-color: #b19cd9;
}
/* 按钮样式 */
/* 预览区样式 */
.preview-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.preview-avatar {
width: 150px;
height: 150px;
border-radius: 50%;
border: 5px solid #fff;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 10px;
}
.preview-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.preview-email {
color: #666;
font-size: 1.2rem;
margin-bottom: 15px;
margin-top:0px;
}
.preview-bio {
width: 100%;
min-height: 100px;
padding: 15px;
border: 2px solid #e6e6fa;
border-radius: 15px;
background-color: #f9f7ff;
font-size: 1.2rem;
color: #666;
}
/* 密码修改区域样式 */
.password-inputs {
display: flex;
gap: 10px;
margin-right: 10px;
}
.password-inputs .form-input {
flex: 1;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.main-content {
flex-direction: column;
}
}
@media (max-width: 768px) {
.container {
flex-direction: column;
}
.sidebar {
width: 100%;
padding: 10px;
}
.menu-item {
border-radius: 25px;
margin-right: 0;
padding: 10px 15px;
text-align: center;
}
.main-content {
padding: 15px;
}
.form-group {
flex-direction: column;
align-items: flex-start;
}
.form-label {
width: 100%;
margin-bottom: 5px;
}
.password-inputs {
flex-direction: column;
}
}
/* 可爱元素:气泡背景 */
.bubble {
position: absolute;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.3);
z-index: -1;
animation: float 15s ease-in-out infinite;
}
.bubble-1 {
width: 100px;
height: 100px;
bottom: 50px;
left: 20px;
animation-delay: 0s;
}
.bubble-2 {
width: 60px;
height: 60px;
top: 100px;
left: 40px;
animation-delay: 2s;
}
.bubble-3 {
width: 80px;
height: 80px;
bottom: 200px;
left: 60px;
animation-delay: 5s;
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-20px);
}
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: fadeIn 0.5s ease forwards;
}
.profile-preview-card {
animation-delay: 0.2s;
}
/* 提示文本样式 */
.form-hint {
font-size: 0.8rem;
color: #9370DB;
margin-top: 5px;
margin-left: 100px;
}
/* 可爱装饰元素 */
.cute-decoration {
position: absolute;
font-size: 1.5rem;
opacity: 0.3;
color: #9370DB;
pointer-events: none;
}
.star-1 {
top: 50px;
right: 20px;
}
.star-2 {
bottom: 100px;
right: 40px;
}
.heart {
bottom: 30px;
right: 30px;
}
</style>

@ -1,135 +0,0 @@
<template>
<div class="forum-home">
<!-- 左侧主内容区 -->
<div class="left-section card">
<!-- 上半部分 -->
<div class="top-section">
<div class="carousel-wrapper">
<Carousel />
</div>
<div class="hot-topic-wrapper">
<h2>📍 今日热点</h2>
<HotTopic
v-for="(item, index) in hotTopics"
:key="index"
:title="item.title"
:link="item.link"
/>
</div>
</div>
<el-divider content-position="right">欢迎来到UniLife</el-divider>
<!-- 下半部分帖子 -->
<div class="posts-section">
<h2>帖子</h2>
<PostCard
v-for="(post, index) in posts"
:key="index"
:post="post"
/>
</div>
</div>
<!-- 右侧今日行程 -->
<div class="right-section">
<h2>📅 今日行程</h2>
<ScheduleCard />
</div>
</div>
</template>
<script setup lang="ts">
import Carousel from '@/components/Carousel.vue'
import PostCard from '@/components/PostCard.vue'
import HotTopic from '@/components/HotTopic.vue'
import ScheduleCard from '@/components/ScheduleCard.vue'
const hotTopics = [
{ title: '这是一个标题,一个热门帖子的标题', link: '/post/1' },
{ title: '这是一个标题,一个热门帖子的标题', link: '/post/2' },
{ title: '这是一个标题,一个热门帖子的标题', link: '/post/3' }
]
const posts = [
{
id: 1,
title: '蚂蚁金服设计平台简介',
tags: ['Ant Design', '设计语言', '蚂蚁金服'],
excerpt: '段落示意:这是帖子的部分具体内容……',
link: '/post/1'
},
{
id: 2,
title: '蚂蚁金服设计平台简介',
tags: ['Ant Design', '设计语言', '蚂蚁金服'],
excerpt: '段落示意:这是帖子的部分具体内容……',
link: '/post/2'
}
]
</script>
<style scoped lang="scss">
.forum-home {
display: flex;
width:92%;
height:98%;
padding-top:75px;
gap: 40px; // 🔧
.left-section {
flex: 3;
display: flex;
flex-direction: column;
gap: 30px; // 🔧
background: linear-gradient(to bottom right, #f7f1ff, #ffffff);
}
.top-section {
display: flex;
gap: 24px; // 🔧
height: 320px;
background-color:transparent;
.carousel-wrapper {
flex: 2;
border-radius: 8px;
overflow: hidden;
}
.hot-topic-wrapper {
flex: 1;
padding: 16px;
background-color: #fef6ff;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
gap: 12px;
}
}
.posts-section {
display: flex;
flex-direction: column;
gap: 20px;
h2 {
margin-bottom: 12px;
}
}
.right-section {
flex: 1;
min-width: 240px;
padding: 16px;
background-color: #f9f7ff;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
background: linear-gradient(to bottom right, #f7f1ff, #ffffff);
h2 {
margin-bottom: 16px;
}
}
}
</style>

@ -1,21 +1,352 @@
<script set lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import { ref, onMounted } from 'vue';
import { useUserStore } from '../stores';
export default defineComponent({ const userStore = useUserStore();
name:'Home',
//
const userPosts = ref([
{
id: 1,
title: '大学生活经验分享',
content: '分享一些大学生活的经验和技巧...',
createTime: '2023-05-01',
likes: 25,
comments: 10
},
{
id: 2,
title: '考研复习计划',
content: '分享我的考研复习计划和方法...',
createTime: '2023-05-15',
likes: 42,
comments: 18
},
{
id: 3,
title: '校园活动推荐',
content: '推荐几个值得参加的校园活动...',
createTime: '2023-06-01',
likes: 15,
comments: 5
}
]);
//
const userStats = ref({
totalPosts: 3,
totalLikes: 82,
totalComments: 33,
totalViews: 256
});
onMounted(async () => {
//
if (!userStore.userInfo) {
await userStore.fetchUserInfo();
}
// API
}); });
</script> </script>
<template> <template>
<h1>个人主页</h1> <div class="home-page">
<p>还没做 <div class="page-header">
期望完成 <h1>个人主页</h1>
查看已发帖 <p>欢迎回来{{ userStore.userInfo?.username || '用户' }}</p>
查看已发帖的情况 </div>
编辑帖子
</p> <!-- 统计卡片 -->
<div class="stats-container">
<div class="stat-card">
<div class="stat-icon">
<el-icon><document /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ userStats.totalPosts }}</div>
<div class="stat-label">发布帖子</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<el-icon><star /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ userStats.totalLikes }}</div>
<div class="stat-label">获得点赞</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<el-icon><chat-dot-round /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ userStats.totalComments }}</div>
<div class="stat-label">收到评论</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<el-icon><view /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ userStats.totalViews }}</div>
<div class="stat-label">帖子浏览</div>
</div>
</div>
</div>
<!-- 最近帖子 -->
<div class="recent-posts">
<div class="section-header">
<h2>最近发布的帖子</h2>
<button class="btn btn-primary">发布新帖</button>
</div>
<div class="posts-list">
<div v-if="userPosts.length === 0" class="empty-state">
<p>你还没有发布过帖子</p>
<button class="btn btn-primary">立即发布</button>
</div>
<div v-else class="post-card" v-for="post in userPosts" :key="post.id">
<div class="post-header">
<h3 class="post-title">{{ post.title }}</h3>
<span class="post-date">{{ post.createTime }}</span>
</div>
<p class="post-content">{{ post.content }}</p>
<div class="post-footer">
<div class="post-stats">
<div class="post-stat">
<el-icon><star /></el-icon>
<span>{{ post.likes }}</span>
</div>
<div class="post-stat">
<el-icon><chat-dot-round /></el-icon>
<span>{{ post.comments }}</span>
</div>
</div>
<div class="post-actions">
<button class="btn btn-text">编辑</button>
<button class="btn btn-text">删除</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template> </template>
<style scoped> <style scoped>
.home-page {
max-width: 1000px;
margin: 0 auto;
}
.page-header {
margin-bottom: var(--spacing-xl);
}
.page-header h1 {
font-size: var(--font-size-xxl);
color: var(--primary-color);
margin-bottom: var(--spacing-xs);
}
.page-header p {
color: var(--text-secondary);
}
.stats-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
}
.stat-card {
background-color: var(--bg-primary);
border-radius: var(--border-radius-md);
padding: var(--spacing-lg);
display: flex;
align-items: center;
box-shadow: var(--shadow-sm);
transition: transform var(--transition-normal), box-shadow var(--transition-normal);
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-md);
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: var(--border-radius-full);
background-color: var(--primary-light);
display: flex;
justify-content: center;
align-items: center;
margin-right: var(--spacing-md);
color: white;
font-size: var(--font-size-xl);
}
.stat-content {
flex: 1;
}
.stat-value {
font-size: var(--font-size-xl);
font-weight: 700;
color: var(--text-primary);
}
.stat-label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
}
.section-header h2 {
font-size: var(--font-size-xl);
color: var(--text-primary);
}
.posts-list {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.post-card {
background-color: var(--bg-primary);
border-radius: var(--border-radius-md);
padding: var(--spacing-lg);
box-shadow: var(--shadow-sm);
transition: transform var(--transition-normal), box-shadow var(--transition-normal);
}
.post-card:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-md);
}
.post-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
}
.post-title {
font-size: var(--font-size-lg);
color: var(--text-primary);
margin: 0;
}
.post-date {
font-size: var(--font-size-sm);
color: var(--text-light);
}
.post-content {
color: var(--text-secondary);
margin-bottom: var(--spacing-lg);
line-height: 1.6;
}
.post-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.post-stats {
display: flex;
gap: var(--spacing-md);
}
.post-stat {
display: flex;
align-items: center;
color: var(--text-light);
}
.post-stat .el-icon {
margin-right: var(--spacing-xs);
}
.post-actions {
display: flex;
gap: var(--spacing-sm);
}
.btn-text {
background: none;
color: var(--primary-color);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--border-radius-sm);
transition: background-color var(--transition-normal);
}
.btn-text:hover {
background-color: rgba(147, 112, 219, 0.1);
}
.empty-state {
text-align: center;
padding: var(--spacing-xl);
color: var(--text-light);
}
.empty-state p {
margin-bottom: var(--spacing-lg);
}
/* 响应式设计 */
@media (max-width: 768px) {
.stats-container {
grid-template-columns: repeat(2, 1fr);
}
}
</style> @media (max-width: 480px) {
.stats-container {
grid-template-columns: 1fr;
}
.post-header {
flex-direction: column;
align-items: flex-start;
}
.post-date {
margin-top: var(--spacing-xs);
}
.post-footer {
flex-direction: column;
gap: var(--spacing-md);
}
.post-actions {
width: 100%;
justify-content: flex-end;
}
}
</style>

@ -1,542 +0,0 @@
<script setup lang="ts">
import request from '../utils/request' //
import * as yup from 'yup'
import {useRouter} from 'vue-router'
import { useField, useForm, } from 'vee-validate'
import { ref } from 'vue'
import { computed } from 'vue'
const token = ref<string>('')
const router = useRouter()
const testEmail = ref('test@example.com')
const testPassword = ref('123456')
//
const transRate = ref('90')
const switchLogin = ref(true);
const switchLoginMethod = ref<boolean>(true);
const switchLoginMethodEvent = () =>{
switchLoginMethod.value = !switchLoginMethod.value;
if(switchLoginMethod.value) {
login_password.value = "";
login_password_email.value = login_vericode_email.value;
} else {
login_vericode.value = "";
login_vericode_email.value = login_password_email.value;
}
}
const form_box = ref<HTMLElement | null>(null);
//
const switchLoginEvent = () =>{
switchLogin.value = !switchLogin.value;
switchLogin.value ? transRate.value = '0' : transRate.value = '90';
if(form_box.value)
{
form_box.value.style.transform = `translateX(${transRate.value}%)`;
}
if(switchLoginMethod.value) {
login_password.value = login_vericode.value = "";
login_password_email.value = login_vericode_email.value = register_email.value;
} else {
register_password.value = register_verifyPassword.value = register_vericode.value = "";
login_vericode_email.value = login_password_email.value = register_email.value;
}
}
//
//
const RegisterForm = useForm({
validationSchema : yup.object({
register_email: yup.string().email("请输入邮箱").required("请输入正确的邮箱"),
register_password: yup.string().min(6,"密码至少6位").required("请输入密码"),
register_verifyPassword: yup.string().oneOf([yup.ref('register_verifyPassword')],"两次密码不一致").required("请确认密码"),
register_vericode: yup.string().required("请输入验证码")
}),
})
const {value: register_email} = useField('register_email')
const {value: register_password} = useField ('register_password')
const {value: register_verifyPassword} = useField('register_verifyPassword')
const {value: register_vericode} = useField('register_vericode')//
//
//
const LoginPasswordForm = useForm({
validationSchema : yup.object({
login_password_email: yup.string().email("请输入邮箱").required("请输入正确的邮箱"),
login_password: yup.string().min(6,"密码至少6位").required("请输入密码")
}),
})
const{value: login_password_email} = useField('login_password_email')
const{value: login_password} = useField('login_password')
//
const LoginEmailForm = useForm({
validationSchema : yup.object({
login_vericode_email: yup.string().email("请输入邮箱").required("请输入正确的邮箱"),
login_vericode: yup.string().required("请输入验证码")
}),
})
const{value:login_vericode_email} = useField('login_vericode_email')
const{value: login_vericode} = useField('login_vericode')
//
const showErrors = ref(false)
const ErrorsMessage = ref('')
//
const checkErrors = () => {
if(RegisterForm.errors.value.register_email || RegisterForm.errors.value.register_password || RegisterForm.errors.value.register_verifyPassword || RegisterForm.errors.value.register_vericode) {
showErrors.value = true
ErrorsMessage.value = '请检查输入是否正确'
}else if(LoginPasswordForm.errors.value.login_password_email || LoginPasswordForm.errors.value.login_password) {
showErrors.value = true
ErrorsMessage.value = '请检查输入是否正确'
}else if(LoginEmailForm.errors.value.login_vericode_email || LoginEmailForm.errors.value.login_vericode) {
showErrors.value = true
ErrorsMessage.value = '请检查输入是否正确'
}
}
//
const onRegisterSubmit = () => {
LoginEmailForm.resetForm();
LoginPasswordForm.resetForm();
console.log('函数调用成功')
RegisterForm.handleSubmit((values) => {
console.log('表单调用成功',values)
testcode().then((res) => {
if(res.code === 200) {
register().then((res) => {
if(res.code === 200) {
console.log('注册成功')
localStorage.setItem('token', res.data.token)
router.push({path:'/personal'})
} else {
console.log('注册失败')
}
})
} else {
console.log('注册失败')
}
})
},
(errors)=>{
console.log('注册表单调用失败',errors)
})();
}
//
const onLoginSubmit = () => {
if(!switchLoginMethod.value) {
LoginEmailForm.resetForm();
RegisterForm.resetForm();
LoginPasswordForm.handleSubmit((values) => {
console.log('表单调用成功',values)
if(login_password_email.value == testEmail.value && login_password.value == testPassword.value) {
console.log('测试登录成功')
console.log(router)
router.push({name:'Home'})
} else {
login().then((res) => {
if(res.code === 200) {
console.log('密码登录成功')
localStorage.setItem('token', res.data.token)
} else {
console.log('登录失败')
}
})
}
},
(errors) => {
console.log('密码登录表单调用失败',errors)
})();
}else {
LoginPasswordForm.resetForm();
RegisterForm.resetForm();
LoginEmailForm.handleSubmit(() => {
testcode().then((res) => {
if(res.code === 200) {
console.log('邮箱登录成功')
localStorage.setItem('token', res.data.token)
} else {
console.log('登录失败')
}
})
},
(errors)=>{
console.log("邮箱登录表单调用失败",errors)
})();
}
}
//axios
//
const email = computed(()=>register_email.value ?? login_password_email.value ?? login_vericode_email.value)
const password = computed(()=>register_password.value ?? login_password.value)
const vericode = computed(()=>register_vericode.value ?? login_vericode.value)
//
async function emailcode(){
const res = await request.get('/users/code', {
data:{
email: email.value
}
})
console.log("success")
}
//
async function testcode() {
const res = await request.post('/users/login/code', {
data: {
email: email.value,
code: vericode.value
}
})
return res.data;
}
async function register(){
const res = await request.post('/users/register', {
data:{
email: email.value,
password: password.value,
username:null,
nickname:null,
studentId:null,
department:null,
major:null,
grade:null,
}
})
return res.data;
}
async function login(){
const res = await request.post('/users/login', {
data:{
email: email.value,
password: password.value
}
})
return res.data;
}
</script>
<template>
<title>登录</title>
<!-- 错误信息显示现在无法做到精确显示 -->
<el-alert
class = "error-msg"
v-if="showErrors"
:title=ErrorsMessage
type="error"
:closable="true"
@close="showErrors = false"
show-icon = "true"
center/>
<!-- 登录注册表单 -->
<div class = "container">
<div ref = "form_box" class = "form-box">
<!-- 注册表单 -->
<div class = "register-box" :class = "{hidden: !switchLogin}">
<h1>register</h1>
<div class="email-vericode">
<input type="text" placeholder="邮箱" v-model="register_email">
<button class="vericode-btn" @click = "emailcode">获取验证码</button>
</div>
<input type="password" placeholder="密码" v-model="register_password">
<input type ="password" placeholder = "确认密码" v-model = "register_verifyPassword">
<input type="text" placeholder="验证码" v-model="register_vericode">
<button @click = "onRegisterSubmit(),checkErrors()">注册</button>
</div>
<!-- 登录表单 -->
<div class = "login-box" :class ="{hidden: switchLogin}">
<!--密码登录-->
<div class = password-method :class="{hidden: switchLoginMethod}">
<h1>login</h1>
<input type="text" placeholder="邮箱" v-model="login_password_email">
<input type="password" placeholder="密码" v-model="login_password">
<button class = "switch-btn" @click="switchLoginMethodEvent"></button>
</div>
<!--邮箱登录-->
<div class = "email-method" :class ="{hidden: !switchLoginMethod}">
<h1>login</h1>
<input class ="email-text" type="text" placeholder="邮箱" v-model="login_vericode_email">
<button class="email-btn" @click="emailcode"></button>
<input type="text" placeholder="验证码" v-model="login_vericode">
<button class = "switch-btn" @click="switchLoginMethodEvent"></button>
</div>
<button @click="onLoginSubmit(),checkErrors()"></button>
</div>
</div>
<div>
</div>
<div class="con-box right">
<h2>欢迎来到<span>UniLife学生论坛</span></h2>
<p>这是一个专属于大学生的论坛
<br>你可以在这里发表自己的观点
<br>也可以在这里找到志同道合的朋友</p>
<img src = "../../public/images/LogPage1.jpg" alt=""></img>
<p>已有账号</p>
<button id= "login" @click="switchLoginEvent"></button>
</div>
<div class="con-box left">
<h2>欢迎回到<span>UniLife学生论坛</span></h2>
<p>快来看看论坛新来的讯息和同学们的消息吧</p>
<img src = "../../public/images/LogPage2.jpg" alt=""></img>
<p>没有账户</p>
<button id= "register" @click="switchLoginEvent"></button>
</div>
</div>
</template>
<style scoped>
*{
margin: 0;
padding: 0;
}
body{
height: 100vh;;
/*弹性布局,水平垂直居中*/
justify-content: center;
align-items: center;
/*渐变背景*/
background: linear-gradient(200deg, #f3e7e9, #2a4b7c);
}
.container{
background-color: #fff;
width:943.8px; /* 原值786.5px再增加20% */
height:653.4px; /* 原值544.5px再增加20% */
border-radius: 5px;
/* 阴影 */
box-shadow:5px 5px 5px rgba(0,0,0,0.1);
/* 相对定位 */
position: relative;
}
.error-msg{
width:30%;
position: absolute;
top:5%;
left:35%;
display: flex;
}
.form-box{
/* 独立 */
position: absolute;
top:-7%;
left:2.5%;
background-color: #d3b7d8;
width:470px; /* 原值387.2px再增加20% */
height:750px; /* 原值665.5px再增加20% */
border-radius: 5px;
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
display: flex;
justify-content: center;
align-items:center;
z-index: 2;
/* 透明度 */
/* 动画过度,先加速后减速 */
transition:transform 0.5s ease-in-out;
}
.register-box{
display:flex;
flex-direction: column;
align-items: center;
width:100%;
height:60%
}
.login-box{
display:flex;
flex-direction: column;
align-items: center;
width:100%;
height:60%
}
.email-btn{
width:20% !important;
margin-top:20px !important;
position:relative !important;
top:8% !important;
left:2% !important;
}
.email-text{
width: 50% !important;
}
.hidden{
display: none;
transition:0.5s;
}
h1{
text-align: center;
margin-bottom:25px;
/* 大写 */
text-transform:uppercase;
color: #fff;
letter-spacing:5px;
}
input{
background-color: transparent;
width:70%;
height:100px;
color:#fff;
border:none;
/* 下边框样式 */
border-bottom:1px solid rgba(255,255,255,0.4);
padding: 12px,0;
text-indent:10px;
margin: 10px 0;
font-size:19px;
letter-spacing:2px;
}
input::placeholder{
font-size: 19px;
color:#fff;
}
input:focus::placeholder{
opacity:0;
}
input:focus{
color:#a262ad;
outline:none;
border-bottom: 1px solid #a262ad80;
transition:0.5s;
}
.form-box button{
width: 70%;
margin-top:35px;
background-color:#f6f6f6;
outline:none;
border-radius: 8px;
padding:13px;
color:#a262ad;
letter-spacing:2px;
border: none;
cursor:pointer;
}
.vericode-btn{
width: 30% !important;
padding:6px;
border-radius: 5px;
letter-spacing: 4px;
outline:none;
cursor:pointer;
}
.email-vericode {
display: flex;
align-items: center;
width: 70%;
margin: 10px 0;
}
.email-vericode input {
width: 70%;
margin-right: 10px;
}
.email-vericode input::placeholder {
font-size: 19px;
color: #fff;
}
.form-box button:hover{
background-color:#a262ad;
color:#f6f6f6;
transition:background-color 0.5s ease;
}
.con-box{
width:50%;
display:flex;
flex-direction: column;
justify-content: center;
align-items: center;
position:absolute;
top:50%;
transform:translateY(-50%);
}
.con-box.left{
left:-2%;
}
.con-box.right{
right:-2%;
}
.con-box h2{
color:#8e9aaf;
font-size:25px;
font-weight:bold;
letter-spacing: 3px;
text-align:center;
margin-bottom:4px;
}
.con-box p{
font-size:15px;
letter-spacing: 2px;
color:#8e9aaf;
text-align:center;
}
.con-box span{
color: #d3b7d8;
}
.con-box img{
width:217.8px; /* 原值181.5px再增加20% */
height:217.8px; /* 原值181.5px再增加20% */
opacity:0.9;
margin:40px 0;
}
.con-box button{
margin-top:3%;
background-color:#fff;
color:#a262ad;
padding:6px;
border-radius: 5px;
letter-spacing: 1px;
outline:none;
cursor:pointer;
}
.con-box button:hover{
background-color:#d3b7d8;
color:#fff;
outline:none;
}
</style>

@ -0,0 +1,502 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { useUserStore } from '../stores';
import { useEmailCode } from '../hooks/useEmailCode';
const router = useRouter();
const userStore = useUserStore();
const { sendEmailCode, countdown } = useEmailCode();
//
const formMode = ref<'login-password' | 'login-code' | 'register'>('login-password');
const loginEmail = ref('');
const loginPassword = ref('');
const loginCodeEmail = ref('');
const loginCode = ref('');
const registerEmail = ref('');
const registerPassword = ref('');
const confirmPassword = ref('');
const registerCode = ref('');
//
const switchMode = (mode: 'login-password' | 'login-code' | 'register') => {
formMode.value = mode;
resetForms(); //
};
// - ref
const resetForms = () => {
loginEmail.value = '';
loginPassword.value = '';
loginCodeEmail.value = '';
loginCode.value = '';
registerEmail.value = '';
registerPassword.value = '';
confirmPassword.value = '';
registerCode.value = '';
};
//
const handlePasswordLogin = async () => {
try {
// store action (true/false)
const success = await userStore.login(loginEmail.value, loginPassword.value);
if (success) {
// store true
ElMessage.success('登录成功'); //
router.push('/personal');
} else {
}
} catch (error: any) {
console.error("登录组件捕获异常:", error);
// 线
ElMessage.error(error.message || '登录请求处理异常');
}
};
//
const handleCodeLogin = async () => {
try {
// store action (true/false)
const success = await userStore.loginWithCode(loginCodeEmail.value, loginCode.value);
if (success) {
ElMessage.success('登录成功'); //
router.push('/personal');
} else {
// store
// ElMessage.error('');
}
} catch (error: any) {
console.error("验证码登录组件捕获异常:", error);
ElMessage.error(error.message || '验证码登录处理异常');
}
};
//
const handleRegister = async () => {
try {
// store action (true/false)
const success = await userStore.register(
registerEmail.value,
registerPassword.value,
registerCode.value
);
if (success) {
ElMessage.success('注册成功,已自动登录'); //
router.push('/personal');
} else {
}
} catch (error: any) {
console.error("注册组件捕获异常:", error);
ElMessage.error(error.message || '注册请求处理异常');
}
};
// - string .value
const handleSendCode = async (email: string) => {
if (!email) { //
ElMessage.warning('请先输入邮箱');
return;
}
try {
await sendEmailCode(email);
// ElMessage.success(''); // sendEmailCode
} catch (error: any) {
console.error("发送验证码失败:", error);
}
};
const isLoginPasswordActive = computed(() => formMode.value === 'login-password');
const isLoginCodeActive = computed(() => formMode.value === 'login-code');
const isRegisterActive = computed(() => formMode.value === 'register');
const codeButtonText = computed(() => {
return countdown.value > 0 ? `${countdown.value}秒后重新获取` : '获取验证码';
});
</script>
<template>
<div class="login-page">
<div class="login-container">
<div class="form-container">
<div class="form-header">
<h1>{{ isRegisterActive ? '注册账号' : '欢迎回来' }}</h1>
<p>{{ isRegisterActive ? '创建你的UniLife账号' : '登录你的UniLife账号' }}</p>
</div>
<form v-if="isLoginPasswordActive" @submit.prevent="handlePasswordLogin" class="login-form">
<div class="form-group">
<label for="email">邮箱</label>
<input
id="email"
type="email"
v-model="loginEmail" placeholder="请输入邮箱"
class="form-input"
>
<div class="error-message"></div> </div>
<div class="form-group">
<label for="password">密码</label>
<input
id="password"
type="password"
v-model="loginPassword" placeholder="请输入密码"
class="form-input"
>
<div class="error-message"></div> </div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">登录</button>
<div class="form-links">
<a href="#" @click.prevent="switchMode('login-code')">验证码登录</a>
<a href="#" @click.prevent="switchMode('register')">注册账号</a>
</div>
</div>
</form>
<form v-if="isLoginCodeActive" @submit.prevent="handleCodeLogin" class="login-form">
<div class="form-group">
<label for="codeEmail">邮箱</label>
<input
id="codeEmail"
type="email"
v-model="loginCodeEmail" placeholder="请输入邮箱"
class="form-input"
>
<div class="error-message"></div> </div>
<div class="form-group">
<label for="code">验证码</label>
<div class="code-input-group">
<input
id="code"
type="text"
v-model="loginCode" placeholder="请输入验证码"
class="form-input"
>
<button
type="button"
class="code-btn"
@click="handleSendCode(loginCodeEmail)" :disabled="countdown > 0"
>
{{ codeButtonText }}
</button>
</div>
<div class="error-message"></div> </div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">登录</button>
<div class="form-links">
<a href="#" @click.prevent="switchMode('login-password')">密码登录</a>
<a href="#" @click.prevent="switchMode('register')">注册账号</a>
</div>
</div>
</form>
<form v-if="isRegisterActive" @submit.prevent="handleRegister" class="login-form">
<div class="form-group">
<label for="registerEmail">邮箱</label>
<input
id="registerEmail"
type="email"
v-model="registerEmail" placeholder="请输入邮箱"
class="form-input"
>
<div class="error-message"></div> </div>
<div class="form-group">
<label for="registerPassword">密码</label>
<input
id="registerPassword"
type="password"
v-model="registerPassword" placeholder="请输入密码"
class="form-input"
>
<div class="error-message"></div> </div>
<div class="form-group">
<label for="confirmPassword">确认密码</label>
<input
id="confirmPassword"
type="password"
v-model="confirmPassword" placeholder="请再次输入密码"
class="form-input"
>
<div class="error-message"></div> </div>
<div class="form-group">
<label for="registerCode">验证码</label>
<div class="code-input-group">
<input
id="registerCode"
type="text"
v-model="registerCode" placeholder="请输入验证码"
class="form-input"
>
<button
type="button"
class="code-btn"
@click="handleSendCode(registerEmail)" :disabled="countdown > 0"
>
{{ codeButtonText }}
</button>
</div>
<div class="error-message"></div> </div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">注册</button>
<div class="form-links">
<a href="#" @click.prevent="switchMode('login-password')">已有账号去登录</a>
</div>
</div>
</form>
</div>
<div class="info-container">
<div class="info-content">
<h2>欢迎来到 <span>UniLife</span> 学生论坛</h2>
<p>这是一个专属于大学生的论坛你可以在这里发表自己的观点也可以在这里找到志同道合的朋友</p>
<div class="info-image">
<img src="/images/LogPage1.jpg" alt="UniLife">
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.login-page {
width: 100%;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: var(--bg-gradient); /* 保留 CSS 变量 */
padding: var(--spacing-lg); /* 保留 CSS 变量 */
}
.login-container {
width: 100%;
max-width: 1000px;
min-height: 600px;
display: flex;
border-radius: var(--border-radius-lg); /* 保留 CSS 变量 */
overflow: hidden;
box-shadow: var(--shadow-lg); /* 保留 CSS 变量 */
background-color: var(--bg-primary); /* 保留 CSS 变量 */
}
.form-container {
flex: 1;
padding: var(--spacing-xl); /* 保留 CSS 变量 */
display: flex;
flex-direction: column;
}
.form-header {
margin-bottom: var(--spacing-xl); /* 保留 CSS 变量 */
text-align: center;
}
.form-header h1 {
color: var(--primary-color); /* 保留 CSS 变量 */
font-size: var(--font-size-xxl); /* 保留 CSS 变量 */
margin-bottom: var(--spacing-sm); /* 保留 CSS 变量 */
}
.form-header p {
color: var(--text-secondary); /* 保留 CSS 变量 */
}
.login-form {
flex: 1;
display: flex;
flex-direction: column;
}
.form-group {
margin-bottom: var(--spacing-lg); /* 保留 CSS 变量 */
}
.form-group label {
display: block;
margin-bottom: var(--spacing-sm); /* 保留 CSS 变量 */
color: var(--text-secondary); /* 保留 CSS 变量 */
font-weight: 500;
}
.form-input {
width: 100%;
padding: var(--spacing-md); /* 保留 CSS 变量 */
border: 2px solid var(--border-color); /* 保留 CSS 变量 */
border-radius: var(--border-radius-md); /* 保留 CSS 变量 */
font-size: var(--font-size-md); /* 保留 CSS 变量 */
transition: border-color var(--transition-normal); /* 保留 CSS 变量 */
box-sizing: border-box; /* 添加这个防止 padding 影响总宽度 */
}
.form-input:focus {
border-color: var(--primary-light); /* 保留 CSS 变量 */
outline: none;
}
.code-input-group {
display: flex;
gap: var(--spacing-md); /* 保留 CSS 变量 */
}
.code-input-group .form-input {
flex: 1;
}
.code-btn {
padding: var(--spacing-sm) var(--spacing-md); /* 保留 CSS 变量 */
background-color: var(--primary-color); /* 保留 CSS 变量 */
color: white;
border: none;
border-radius: var(--border-radius-md); /* 保留 CSS 变量 */
cursor: pointer;
transition: background-color var(--transition-normal); /* 保留 CSS 变量 */
white-space: nowrap;
}
.code-btn:hover:not(:disabled) {
background-color: var(--primary-dark); /* 保留 CSS 变量 */
}
.code-btn:disabled {
background-color: var(--text-light); /* 保留 CSS 变量 */
cursor: not-allowed;
}
.error-message {
/* color: #ff4d4f; */ /* 不再需要特定颜色 */
font-size: var(--font-size-sm); /* 保留 CSS 变量 */
margin-top: var(--spacing-xs); /* 保留 CSS 变量 */
min-height: 20px; /* 保留最小高度防止布局跳动 */
}
.form-actions {
margin-top: var(--spacing-lg); /* 保留 CSS 变量 */
display: flex;
flex-direction: column;
align-items: center;
}
.form-actions .btn {
width: 100%;
padding: var(--spacing-md); /* 保留 CSS 变量 */
font-size: var(--font-size-lg); /* 保留 CSS 变量 */
/* 假设按钮样式由 .btn 和 .btn-primary 类定义 */
}
/* 你可能需要确保 .btn 和 .btn-primary 有定义,例如:*/
.btn {
cursor: pointer;
border-radius: var(--border-radius-md);
transition: background-color var(--transition-normal), color var(--transition-normal), border-color var(--transition-normal);
border: 1px solid transparent;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.btn-primary:hover {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
.form-links {
margin-top: var(--spacing-lg); /* 保留 CSS 变量 */
display: flex;
justify-content: space-between;
width: 100%;
}
.form-links a {
color: var(--primary-color); /* 保留 CSS 变量 */
text-decoration: none;
transition: color var(--transition-normal); /* 保留 CSS 变量 */
}
.form-links a:hover {
color: var(--primary-dark); /* 保留 CSS 变量 */
text-decoration: underline;
}
.info-container {
flex: 1;
background-color: var(--primary-light); /* 保留 CSS 变量 */
padding: var(--spacing-xl); /* 保留 CSS 变量 */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
position: relative;
overflow: hidden;
}
.info-content {
text-align: center;
z-index: 1;
}
.info-content h2 {
font-size: var(--font-size-xl); /* 保留 CSS 变量 */
margin-bottom: var(--spacing-lg); /* 保留 CSS 变量 */
}
.info-content span {
font-weight: 700;
}
.info-content p {
margin-bottom: var(--spacing-xl); /* 保留 CSS 变量 */
line-height: 1.6;
}
.info-image {
width: 80%;
max-width: 300px;
margin: 0 auto;
border-radius: var(--border-radius-lg); /* 保留 CSS 变量 */
overflow: hidden;
box-shadow: var(--shadow-md); /* 保留 CSS 变量 */
}
.info-image img {
width: 100%;
height: auto;
display: block; /* 移除图片底部空隙 */
object-fit: cover;
}
/* 响应式设计 */
@media (max-width: 768px) {
.login-container {
flex-direction: column;
min-height: auto; /* 调整高度 */
}
.info-container {
display: none; /* 在小屏幕上隐藏右侧信息 */
}
.form-container {
padding: var(--spacing-lg); /* 使用变量 */
}
.form-header h1 {
font-size: var(--font-size-xl); /* 使用变量 */
}
}
</style>

@ -0,0 +1,101 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
const goHome = () => {
router.push('/');
};
const goBack = () => {
router.back();
};
</script>
<template>
<div class="not-found">
<div class="not-found-content">
<h1>404</h1>
<h2>页面不存在</h2>
<p>抱歉您访问的页面不存在或已被移除</p>
<div class="not-found-actions">
<button class="btn btn-primary" @click="goHome"></button>
<button class="btn btn-secondary" @click="goBack"></button>
</div>
</div>
<div class="not-found-image">
<img src="/images/默认头像.jpg" alt="404">
</div>
</div>
</template>
<style scoped>
.not-found {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: var(--bg-gradient);
padding: var(--spacing-lg);
}
.not-found-content {
text-align: center;
margin-right: var(--spacing-xl);
}
.not-found-content h1 {
font-size: 120px;
color: var(--primary-color);
margin: 0;
line-height: 1;
}
.not-found-content h2 {
font-size: var(--font-size-xxl);
color: var(--text-primary);
margin-bottom: var(--spacing-lg);
}
.not-found-content p {
font-size: var(--font-size-lg);
color: var(--text-secondary);
margin-bottom: var(--spacing-xl);
}
.not-found-actions {
display: flex;
justify-content: center;
gap: var(--spacing-lg);
}
.not-found-image {
max-width: 400px;
}
.not-found-image img {
width: 100%;
height: auto;
}
/* 响应式设计 */
@media (max-width: 768px) {
.not-found {
flex-direction: column;
}
.not-found-content {
margin-right: 0;
margin-bottom: var(--spacing-xl);
}
.not-found-content h1 {
font-size: 80px;
}
.not-found-image {
max-width: 300px;
}
}
</style>

@ -8,12 +8,7 @@
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true, "noUncheckedSideEffectImports": true
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}, },
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": [ "exclude": [

@ -1,10 +1,4 @@
{ {
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"files": [], "files": [],
"references": [ "references": [
{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.app.json" },

@ -4,9 +4,5 @@ import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [vue()],
resolve: {
alias: {
'@': '/src',
}
}
}) })

@ -1,4 +1,3 @@
[TOC]
# 配置前端环境 # 配置前端环境
--- ---
@ -60,10 +59,4 @@ pnpm add vue-router@4
### 安装elementplus的icon库 ### 安装elementplus的icon库
``` ```
pnpm install @element-plus/icons-vue pnpm install @element-plus/icons-vue
```
### 安装scss依赖
```cmd
pnpm add -D sass-embedded
``` ```

@ -0,0 +1,72 @@
3daacf2 (HEAD -> main) Revert
8b57c29 (origin/main, origin/HEAD, czq) Stop tracking
c79ff80 test
6a5cf75 更新
78ef254 tmp
5f87197 分类管理,评论,资源,课程
f4a8658 用户信息管理,帖子模块
f632c54 用户信息管理,帖子模块
ca6ab29 升级为springboot3,更新开发文档
33c72dc 升级为springboot3,更新开发文档
9863456 +logo
cec742a 软件体系结构
9bbc131 原型图
f208ba4 原型图
b772fb5 添加Token
204dd28 原型图/个人设置
5720af5 axios完成
af73bbc 完成个人信息界面的表单验证
a4e4fa5 pics
16cfbd5 Delete '图片素材/2a25599c42b1f75029ac3cf8ccf0d5d7.png'
e9d0c89 ADD file via upload
be57df6 Delete '图片素材/pics'
fd9c972 ADD file via upload
fff3563 Delete '图片素材/需求规格说明图'
91b33f3 Add 需求规格说明图
91ac306 Delete '图片素材/3afd20c18a72d94f41e18259f2039100.png'
239eb5a ADD file via upload
2d43df1 ADD file via upload
c6fcbd2 Delete 'unilife软件需求规格说明书.docx'
5e1b17f ADD file via upload
3c2d331 文档
a48e551 个人界面
af1e136 Merge branch 'main' of https://bdgit.educoder.net/pizvue73f/unilife
8a61ff1 验收测试计划书
9e7d617 开发文档更新
d05eb50 用户信息管理接口
1351060 个人信息展示界面布局完成
561dbc6 Merge branch 'main' of https://bdgit.educoder.net/pizvue73f/unilife
a98ea9c 界面初步修改
83f94ea 需求获取文档
04509d7 完成侧边栏的切换功能缺个人画原型图TAT
d6b42b2 添加了显示IP归属地的功能
3adff4b 侧边栏初步实现
3bde499 修改注册表单错误
4908ad6 前端的/users/code改成post方法,传递json改成正确格式
126c1e9 界面跳转
f80b341 Merge branch 'main' of https://bdgit.educoder.net/pizvue73f/unilife
5e1494e 前端bug修改表单可以正确显示
4483fdf Merge remote-tracking branch 'origin/main'
155c336 jwt实现token
fb05160 错误信息提示完成
f8cd2a1 发送邮箱验证码功能及邮箱密码登录功能
98c9cd6 发送邮箱验证码功能初步实现
5c148bb 后端加了一点成功的返回消息
be45d15 axios连接完毕但是没有相应的返回检查即提示用户密码错误等等错误信息提示依然有错改不对了
a106852 文档
795c3dd 焦点访谈报告
4b23ae9 任务项目书
0b923dd Merge branch 'main' of https://bdgit.educoder.net/pizvue73f/unilife
f88eb51 完成axios交互除了验证码注册和密码登录错误信息显示依旧有bug
86da7d9 开发环境添加
4ae6e4c 代码框架及用户注册接口
283973c 后端init
68837be 完成登录和注册页面,其中登录界面可以选择密码和邮箱两种登录方式
8fd6b41 图片素材
94cb4de Merge branch 'main' of https://bdgit.educoder.net/pizvue73f/unilife
34df869 前端环境
dc564e5 readme
e74e446 readme
8196f94 readme
c112817 README.md
d10ebd1 Initial commit

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742889870113" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6198" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M501.824 32C303.552 32 141.504 176.992 141.504 357.76c0 23.712 2.816 47.104 8.32 69.856l-51.008 114.208a32 32 0 0 0 24.704 44.736c54.272 7.744 76.672 31.168 76.672 77.312v111.552a64 64 0 0 0 64 64h20.704a64 64 0 0 1 64 64V960a32 32 0 0 0 32 32h345.6a32 32 0 0 0 0-64h-313.6v-24.608a128 128 0 0 0-128-128h-20.736v-111.552c0-65.664-32.192-110.688-91.2-131.136l39.872-89.28a31.968 31.968 0 0 0 1.568-21.792 233.088 233.088 0 0 1-8.896-63.904c0-143.712 131.936-261.76 296.32-261.76s296.32 118.016 296.32 261.76a32 32 0 0 0 64 0C862.144 176.992 700.064 32 501.824 32zM904 448a32 32 0 0 0-32 32v360a32 32 0 0 0 64 0V480a32 32 0 0 0-32-32z" p-id="6199"></path><path d="M673.888 466.656c-11.744-25.568-48.416-24.64-58.816 1.536l-132.8 333.76a32 32 0 0 0 59.488 23.68l32.608-81.92c0.576 0.032 1.088 0.32 1.664 0.32h154.848l38.176 83.104a31.968 31.968 0 1 0 58.144-26.72l-153.312-333.76zM599.68 680l47.264-118.72 54.528 118.72H599.68z" p-id="6200"></path></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742890464540" class="icon" viewBox="0 0 1070 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12795" xmlns:xlink="http://www.w3.org/1999/xlink" width="208.984375" height="200"><path d="M845.803922 820.705882H442.980392c-67.764706 0-122.980392-55.215686-122.980392-122.980392V250.980392c0-67.764706 55.215686-122.980392 122.980392-122.980392H476.862745c10.039216 0 18.823529 8.784314 18.82353 18.823529s-8.784314 18.823529-18.82353 18.82353h-33.882353c-47.686275 0-85.333333 38.901961-85.333333 85.333333v445.490196c0 47.686275 38.901961 85.333333 85.333333 85.333334h402.82353c47.686275 0 85.333333-38.901961 85.333333-85.333334V250.980392c0-47.686275-38.901961-85.333333-85.333333-85.333333H638.745098c-10.039216 0-18.823529-8.784314-18.823529-18.82353s8.784314-18.823529 18.823529-18.823529h207.058824c67.764706 0 122.980392 55.215686 122.980392 122.980392v445.490196c0 69.019608-55.215686 124.235294-122.980392 124.235294z" fill="#0B3155" p-id="12796"></path><path d="M564.705882 165.647059h-5.019607c-10.039216 0-18.823529-8.784314-18.82353-18.82353s8.784314-18.823529 18.82353-18.823529h5.019607c10.039216 0 18.823529 8.784314 18.82353 18.823529s-8.784314 18.823529-18.82353 18.82353zM707.764706 931.137255H304.941176c-67.764706 0-122.980392-55.215686-122.980392-122.980392V361.411765c0-67.764706 55.215686-122.980392 122.980392-122.980392 10.039216 0 18.823529 8.784314 18.82353 18.823529s-8.784314 18.823529-18.82353 18.823529c-47.686275 0-85.333333 38.901961-85.333333 85.333334v445.490196c0 47.686275 38.901961 85.333333 85.333333 85.333333h402.82353c47.686275 0 85.333333-38.901961 85.333333-85.333333 0-10.039216 8.784314-18.823529 18.82353-18.82353s18.823529 8.784314 18.823529 18.82353c0 67.764706-55.215686 124.235294-122.980392 124.235294zM680.156863 282.352941H448c-10.039216 0-18.823529-8.784314-18.823529-18.823529s8.784314-18.823529 18.823529-18.82353h232.156863c10.039216 0 18.823529 8.784314 18.823529 18.82353s-7.529412 18.823529-18.823529 18.823529z" fill="#0B3155" p-id="12797"></path><path d="M828.235294 382.745098H448c-10.039216 0-18.823529-8.784314-18.823529-18.823529s8.784314-18.823529 18.823529-18.82353H828.235294c10.039216 0 18.823529 8.784314 18.82353 18.82353s-7.529412 18.823529-18.82353 18.823529zM765.490196 569.72549H448c-10.039216 0-18.823529-8.784314-18.823529-18.823529s8.784314-18.823529 18.823529-18.82353H765.490196c10.039216 0 18.823529 8.784314 18.823529 18.82353s-8.784314 18.823529-18.823529 18.823529zM638.745098 479.372549H448c-10.039216 0-18.823529-8.784314-18.823529-18.823529s8.784314-18.823529 18.823529-18.82353h190.745098c10.039216 0 18.823529 8.784314 18.823529 18.82353s-8.784314 18.823529-18.823529 18.823529z" fill="#0B3155" p-id="12798"></path></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742898782607" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2961" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M586.945923 513.581008c55.067176-27.962865 92.91211-85.125773 92.91211-150.998039 0-93.338828-75.937506-169.276335-169.277358-169.276335s-169.275311 75.937506-169.275311 169.276335c0 65.872267 37.844933 123.034151 92.911086 150.998039-95.652524 32.016181-164.778904 122.45496-164.778904 228.743728 0 11.31572 9.17394 20.491707 20.491707 20.491707s20.491707-9.174963 20.491707-20.491707c0-110.36869 89.791026-200.160739 200.160739-200.160739S710.741413 631.956046 710.741413 742.324736c0 11.31572 9.17394 20.491707 20.491707 20.491707s20.491707-9.174963 20.491707-20.491707C751.723803 636.035968 682.598446 545.598212 586.945923 513.581008zM382.287753 362.582969c0-70.742181 57.552787-128.293945 128.292921-128.293945 70.742181 0 128.293945 57.552787 128.293945 128.293945 0 70.741157-57.552787 128.292921-128.293945 128.292921C439.84054 490.876913 382.287753 433.324126 382.287753 362.582969z" fill="#272636" p-id="2962"></path><path d="M827.871087 196.127889C743.498468 111.757317 631.320573 65.290005 512 65.290005S280.500509 111.756293 196.128913 196.127889C111.756293 280.501532 65.291029 392.678404 65.291029 511.998977s46.465265 231.499491 130.837884 315.872111 196.550515 130.837884 315.871087 130.837884 231.498468-46.465265 315.871087-130.837884S958.708971 631.319549 958.708971 511.998977 912.243707 280.500509 827.871087 196.127889zM512 917.726581c-223.718271 0-405.726581-182.007287-405.726581-405.727605 0-223.718271 182.00831-405.726581 405.726581-405.726581s405.726581 182.007287 405.726581 405.726581C917.726581 735.719294 735.718271 917.726581 512 917.726581z" fill="#272636" p-id="2963"></path></svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742889758081" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2676" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M844.2 395.8m-20.6 0a20.6 20.6 0 1 0 41.2 0 20.6 20.6 0 1 0-41.2 0Z" fill="#3E3A39" p-id="2677"></path><path d="M264.9 390.5c-33.2 0-60.1 27-60.1 60.1s27 60.1 60.1 60.1 60.1-27 60.1-60.1-26.9-60.1-60.1-60.1z m0 78.7c-10.2 0-18.6-8.3-18.6-18.6s8.3-18.6 18.6-18.6 18.6 8.3 18.6 18.6-8.3 18.6-18.6 18.6zM540 249.2c-55.5 0-100.7 45.2-100.7 100.7S484.5 450.6 540 450.6s100.7-45.2 100.7-100.7S595.5 249.2 540 249.2z m0 159.9c-32.6 0-59.2-26.5-59.2-59.2s26.5-59.2 59.2-59.2c32.6 0 59.2 26.5 59.2 59.2s-26.6 59.2-59.2 59.2z" fill="#3E3A39" p-id="2678"></path><path d="M976.9 320.6c-9.4-22.1-20.2-43.4-32.5-63.8 12.3-30.8 20.6-59.3 24.9-85.5 4.3-25.9 4.4-49.3 0.4-69.4-4.4-22.2-13.9-40.7-28.1-55-19.9-19.9-57.8-27.8-112.4-23.3-40 3.3-87 13.5-127.4 27.4C643 27.8 581.1 16 517.3 16 450 16 384.7 29.2 323.2 55.2c-59.4 25.1-112.7 61.1-158.5 106.9-45.8 45.8-81.8 99.1-106.9 158.5-26 61.5-39.2 126.8-39.2 194.1s13.2 132.6 39.2 194.1c25.1 59.4 61.1 112.7 106.9 158.5 16.5 16.5 33.9 31.7 52.3 45.6-23.1 7.7-44.4 12.6-63.1 14.5-34.3 3.5-59.4-2.5-74.8-17.9-24.1-24.1-25.1-74-2.6-140.3 3.7-10.9-2.2-22.7-13-26.3-10.9-3.7-22.7 2.2-26.3 13-35.6 105.2-12.8 157.6 12.6 183 20.6 20.6 49.5 31 86.1 31 7.1 0 14.6-0.4 22.3-1.2 30.1-3.1 64-12.2 101.3-27.1 20.4 12.3 41.7 23.2 63.8 32.6 61.5 26 126.8 39.2 194.1 39.2s132.6-13.2 194.1-39.2c59.4-25.1 112.7-61.1 158.5-106.9 45.8-45.8 81.8-99.1 106.9-158.5 26-61.5 39.2-126.8 39.2-194.1s-13.2-132.6-39.2-194.1zM832.5 65.2c46.6-3.8 71.7 3.3 79.6 11.2 21.7 21.7 20.7 59.9 16.1 88.2-2.5 15.3-6.8 32-12.7 49.8-13.9-18.4-29.1-35.8-45.6-52.3-33.5-33.5-71-61.7-111.9-84.2 25.4-6.4 51.2-10.8 74.5-12.7zM517.3 971.9c-75.9 0-147.5-18.6-210.5-51.4 111-54.6 235.8-148.4 351.9-264.4 44.6-44.6 86.3-90.9 124-137.7 7.2-8.9 5.8-22-3.2-29.2-8.9-7.2-22-5.8-29.2 3.2-36.7 45.6-77.4 90.8-121 134.4-113.6 113.5-235.4 204.9-342.9 257.4-7.7 3.8-15.4 7.3-22.9 10.6C141 812.7 60.1 673 60.1 514.7c0-252.1 205.1-457.2 457.2-457.2 158.3 0 298 80.8 380.1 203.4-8.6 19.4-18.7 39.7-30.3 60.5-5.6 10-2 22.7 8.1 28.3 3.2 1.8 6.7 2.6 10.1 2.6 7.3 0 14.4-3.9 18.2-10.7 7.1-12.7 13.6-25.2 19.6-37.4 32.8 63 51.4 134.6 51.4 210.5 0 252.1-205.1 457.2-457.2 457.2z" fill="#3E3A39" p-id="2679"></path></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

Loading…
Cancel
Save