feat: 初始化智能物联网设备管理系统前后端基础项目

完成项目整体结构搭建,包含:
1. 前端Vue3+TS+Vite+Antd项目基础配置与核心依赖
2. 后端MidwayJS+TypeORM+JWT权限认证框架
3. 完整的用户、角色、权限、日志数据库表结构
4. 核心业务模块API与基础服务实现
5. 全局样式、路由、状态管理配置
This commit is contained in:
andyesfly committed 2026-05-28 16:23:15 +08:00
commit 3d7eb12103
89 files changed
+16085

No files matched your search

+347
View File
@@ -0,0 +1,347 @@
<template>
<div class="login-container">
<div class="bg-decoration">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
<div class="circle circle-3"></div>
</div>
<div class="login-card" :class="{ 'fade-in': true }">
<div class="brand-section">
<div class="logo-icon">
<ThunderboltOutlined />
</div>
<h1 class="title">智能电网设备管理系统</h1>
<p class="subtitle">Smart Grid IoT Management System</p>
</div>
<a-divider />
<a-form ref="formRef" :model="formState" :rules="rules" size="large" @finish="handleLogin">
<a-form-item name="username">
<a-input ref="usernameInputRef" v-model:value="formState.username" placeholder="请输入用户名" allow-clear
:disabled="loading" @pressEnter="focusPassword">
<template #prefix>
<UserOutlined class="input-icon" />
</template>
</a-input>
</a-form-item>
<a-form-item name="password">
<a-input-password ref="passwordInputRef" v-model:value="formState.password" placeholder="请输入密码" allow-clear
:disabled="loading" @pressEnter="submitForm">
<template #prefix>
<LockOutlined class="input-icon" />
</template>
</a-input-password>
</a-form-item>
<a-form-item class="options-row">
<a-checkbox v-model:checked="formState.rememberMe" :disabled="loading">
记住我
</a-checkbox>
<a class="forgot-link" href="javascript:void(0)">忘记密码?</a>
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" :loading="loading" block class="login-btn">
<template #icon v-if="!loading">
<span></span>
</template>
{{ loading ? '登录中...' : '登 录' }}
</a-button>
</a-form-item>
</a-form>
<div class="footer-info">
<p>默认账户:admin / admin123456</p>
</div>
</div>
<div class="copyright">
<p>&copy; 2026 智能电网设备管理系统 All Rights Reserved</p>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { message } from 'ant-design-vue'
import {
UserOutlined,
LockOutlined,
ThunderboltOutlined,
} from '@ant-design/icons-vue'
import { usePermissionStore } from '@/stores/permission'
const router = useRouter()
const route = useRoute()
const permissionStore = usePermissionStore()
const formRef = ref()
const usernameInputRef = ref()
const passwordInputRef = ref()
const loading = ref(false)
const formState = reactive({
username: '',
password: '',
rememberMe: false,
})
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度为3-20个字符', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度为6-20个字符', trigger: 'blur' },
],
}
onMounted(() => {
nextTick(() => {
const savedUsername = localStorage.getItem('remembered_username')
if (savedUsername) {
formState.username = savedUsername
formState.rememberMe = true
}
usernameInputRef.value?.focus()
})
})
function focusPassword() {
passwordInputRef.value?.focus()
}
function submitForm() {
formRef.value?.submit()
}
async function handleLogin() {
try {
loading.value = true
await permissionStore.login(formState.username, formState.password)
if (formState.rememberMe) {
localStorage.setItem('remembered_username', formState.username)
} else {
localStorage.removeItem('remembered_username')
}
message.success('登录成功!')
setTimeout(() => {
const redirect = (route.query.redirect as string) || '/'
router.push(redirect)
}, 500)
} catch (error: any) {
message.error(error.message || '登录失败,请检查用户名和密码')
formState.password = ''
nextTick(() => {
passwordInputRef.value?.focus()
})
} finally {
loading.value = false
}
}
</script>
<style scoped lang="less">
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #7e22ce 100%);
position: relative;
overflow: hidden;
padding: 20px;
}
.bg-decoration {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
overflow: hidden;
.circle {
position: absolute;
border-radius: 50%;
opacity: 0.08;
}
.circle-1 {
width: 600px;
height: 600px;
top: -200px;
right: -100px;
background: #fff;
animation: float 20s ease-in-out infinite;
}
.circle-2 {
width: 400px;
height: 400px;
bottom: -150px;
left: -100px;
background: #fff;
animation: float 15s ease-in-out infinite reverse;
}
.circle-3 {
width: 200px;
height: 200px;
top: 50%;
left: 10%;
background: #fff;
animation: float 12s ease-in-out infinite;
}
}
@keyframes float {
0%,
100% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-30px) rotate(5deg);
}
}
.login-card {
width: 420px;
max-width: 100%;
background: #fff;
border-radius: 12px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
position: relative;
z-index: 1;
animation: slideUp 0.6s ease-out;
&.fade-in {
animation: slideUp 0.6s ease-out;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.brand-section {
text-align: center;
margin-bottom: 10px;
.logo-icon {
margin-bottom: 16px;
:deep(.anticon) {
font-size: 48px;
color: #1890ff;
filter: drop-shadow(0 2px 4px rgba(24, 144, 255, 0.3));
}
}
.title {
font-size: 24px;
font-weight: 600;
color: #1a1a2e;
margin: 0 0 8px 0;
letter-spacing: 1px;
}
.subtitle {
font-size: 14px;
color: #999;
margin: 0;
font-weight: 400;
letter-spacing: 0.5px;
}
}
.options-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px !important;
}
.forgot-link {
color: #1890ff;
font-size: 14px;
transition: color 0.3s;
&:hover {
color: #40a9ff;
}
}
.login-btn {
height: 44px;
font-size: 16px;
font-weight: 500;
border-radius: 6px;
letter-spacing: 4px;
}
.footer-info {
text-align: center;
margin-top: 20px;
color: #bbb;
font-size: 12px;
}
.input-icon {
color: rgba(0, 0, 0, 0.25);
}
.copyright {
position: absolute;
bottom: 20px;
left: 0;
right: 0;
text-align: center;
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
z-index: 1;
p {
margin: 0;
}
}
@media (max-width: 480px) {
.login-card {
width: 100%;
padding: 30px 20px;
}
.brand-section .title {
font-size: 20px;
}
.copyright {
position: relative;
bottom: auto;
margin-top: 20px;
color: rgba(255, 255, 255, 0.5);
}
}
</style>