feat(frontend): 实现用户登录和项目列表功能

- 新增用户登录页面和相关 API
- 实现项目列表展示和基本交互
- 添加全局布局组件和路由守卫
- 优化首页界面,增加未登录状态展示
This commit is contained in:
zhangtao 2025-08-01 18:23:53 +08:00
parent 5fd3dccb3a
commit d6ba24ae33
13 changed files with 977 additions and 219 deletions

View File

@ -1,66 +1,19 @@
<template>
<div id="app">
<t-layout style="height: 100vh;">
<t-header class="header">
<t-head-menu
:value="activeMenu"
theme="light"
@change="handleMenuChange"
>
<div class="logo" slot="logo">
<span class="logo-text">DeployHelper</span>
</div>
<t-menu-item value="home">首页</t-menu-item>
<t-menu-item value="deploy">部署管理</t-menu-item>
<t-menu-item value="files">文件管理</t-menu-item>
<t-menu-item value="logs">日志管理</t-menu-item>
<t-menu-item value="users">用户管理</t-menu-item>
</t-head-menu>
</t-header>
<t-content class="content">
<router-view />
</t-content>
</t-layout>
<!-- 登录页面不显示布局 -->
<router-view v-if="$route.path === '/login'" />
<!-- 主应用布局 -->
<AppLayout v-else>
<router-view />
</AppLayout>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const activeMenu = ref('home')
const handleMenuChange = (value) => {
activeMenu.value = value
router.push(`/${value}`)
}
import AppLayout from '@/components/AppLayout.vue'
</script>
<style scoped>
.header {
background-color: #fff;
border-bottom: 1px solid #e7e7e7;
}
.logo {
display: flex;
align-items: center;
padding: 0 24px;
}
.logo-text {
font-size: 20px;
font-weight: bold;
color: #0052d9;
}
.content {
padding: 24px;
background-color: #f5f5f5;
}
<style>
#app {
font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
}

View File

@ -4,27 +4,27 @@ import api from './index'
export const deployApi = {
// 获取部署列表
getDeployList(params) {
return api.get('/sys_deploy_file/list', { params })
return api.get('/DeployFiles', { params })
},
// 创建部署
createDeploy(data) {
return api.post('/sys_deploy_file/create', data)
return api.post('/DeployFiles/', data)
},
// 获取部署详情
getDeployDetail(id) {
return api.get(`/sys_deploy_file/detail/${id}`)
return api.get(`/DeployFiles/${id}`)
},
// 更新部署
updateDeploy(id, data) {
return api.put(`/sys_deploy_file/update/${id}`, data)
updateDeploy(data) {
return api.put('/DeployFiles/', data)
},
// 删除部署
deleteDeploy(id) {
return api.delete(`/sys_deploy_file/delete/${id}`)
return api.delete(`/DeployFiles/${id}`)
},
// 执行部署

View File

@ -1,9 +1,10 @@
import axios from 'axios'
import { MessagePlugin } from 'tdesign-vue-next'
import auth from '@/utils/auth'
// 创建 axios 实例
// 创建axios实例
const api = axios.create({
baseURL: '/api',
baseURL: import.meta.env.VITE_API_BASE_URL || '/api/v1',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
@ -13,8 +14,8 @@ const api = axios.create({
// 请求拦截器
api.interceptors.request.use(
(config) => {
// 添加 token
const token = localStorage.getItem('token')
// 添加token到请求头
const token = auth.getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
@ -28,25 +29,19 @@ api.interceptors.request.use(
// 响应拦截器
api.interceptors.response.use(
(response) => {
const { data } = response
// 统一处理响应
if (data.code === 200) {
return data.data
} else {
MessagePlugin.error(data.message || '请求失败')
return Promise.reject(new Error(data.message || '请求失败'))
}
return response
},
(error) => {
// 处理错误响应
if (error.response) {
const { status, data } = error.response
const { response } = error
if (response) {
const { status, data } = response
switch (status) {
case 401:
// 未授权清除token并跳转到登录页
auth.logout()
MessagePlugin.error('登录已过期,请重新登录')
localStorage.removeItem('token')
window.location.href = '/login'
break
case 403:
@ -59,12 +54,10 @@ api.interceptors.response.use(
MessagePlugin.error('服务器内部错误')
break
default:
MessagePlugin.error(data.message || '网络错误')
MessagePlugin.error(data?.message || '请求失败')
}
} else if (error.request) {
MessagePlugin.error('网络连接失败,请检查网络')
} else {
MessagePlugin.error(error.message || '请求失败')
MessagePlugin.error('网络连接失败,请检查网络设置')
}
return Promise.reject(error)

View File

@ -4,46 +4,51 @@ import api from './index'
export const userApi = {
// 用户登录
login(data) {
return api.post('/sys_user/login', data)
return api.post('/user/login', data)
},
// 用户注册
register(data) {
return api.post('/user/register', data)
},
// 用户登出
logout() {
return api.post('/sys_user/logout')
return api.post('/sysUser/logout')
},
// 获取用户信息
getUserInfo() {
return api.get('/sys_user/info')
return api.get('/sysUser/me')
},
// 获取用户列表
getUserList(params) {
return api.get('/sys_user/list', { params })
return api.post('/sysUser/condition', params)
},
// 创建用户
createUser(data) {
return api.post('/sys_user/create', data)
return api.post('/sysUser/', data)
},
// 更新用户
updateUser(id, data) {
return api.put(`/sys_user/update/${id}`, data)
updateUser(data) {
return api.put('/sysUser/', data)
},
// 删除用户
deleteUser(id) {
return api.delete(`/sys_user/delete/${id}`)
return api.delete(`/sysUser/${id}`)
},
// 重置密码
resetPassword(id, data) {
return api.post(`/sys_user/reset-password/${id}`, data)
// 获取用户详情
getUserDetail(id) {
return api.get(`/sysUser/${id}`)
},
// 修改密码
changePassword(data) {
return api.post('/sys_user/change-password', data)
// 批量删除用户
deleteUsers(ids) {
return api.delete('/sysUser/batch', { data: { ids } })
}
}

View File

@ -0,0 +1,212 @@
<template>
<t-layout style="height: 100vh;">
<t-header class="header">
<div class="header-container">
<t-head-menu
:value="activeMenu"
theme="light"
@change="handleMenuChange"
>
<template #logo>
<div class="logo">
<span class="logo-text">DeployHelper</span>
</div>
</template>
<!-- 首页菜单项 - 未登录也能看到 -->
<t-menu-item value="home">首页</t-menu-item>
<!-- 其他菜单项 - 只有登录后才显示 -->
<template v-if="isLoggedIn">
<t-menu-item value="deploy">部署管理</t-menu-item>
<t-menu-item value="files">文件管理</t-menu-item>
<t-menu-item value="logs">日志管理</t-menu-item>
<t-menu-item value="users">用户管理</t-menu-item>
</template>
</t-head-menu>
<!-- 右侧操作区域 -->
<div class="header-actions">
<!-- 已登录显示用户信息下拉菜单 -->
<t-dropdown
v-if="isLoggedIn"
:options="userMenuOptions"
@click="handleUserMenuClick"
>
<t-button variant="text" class="user-button">
<t-icon name="user" />
<span class="username">{{ userInfo?.username || '用户' }}</span>
<t-icon name="chevron-down" />
</t-button>
</t-dropdown>
<!-- 未登录显示登录按钮 -->
<t-button
v-else
theme="primary"
size="medium"
@click="goToLogin"
>
<t-icon name="user" />
登录
</t-button>
</div>
</div>
</t-header>
<t-content class="content">
<slot />
</t-content>
</t-layout>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { MessagePlugin } from 'tdesign-vue-next'
import { userApi } from '@/api/user'
import auth from '@/utils/auth'
const router = useRouter()
const route = useRoute()
const activeMenu = ref('home')
//
const isLoggedIn = computed(() => auth.isLoggedIn())
const userInfo = computed(() => auth.getUserInfo())
//
const userMenuOptions = [
{
content: '个人信息',
value: 'profile',
icon: 'user'
},
{
content: '修改密码',
value: 'change-password',
icon: 'lock-on'
},
{
content: '退出登录',
value: 'logout',
icon: 'logout'
}
]
//
const handleMenuChange = (value) => {
activeMenu.value = value
//
if (!isLoggedIn.value && value !== 'home') {
router.push('/login')
return
}
router.push(`/${value}`)
}
//
const goToLogin = () => {
router.push('/login')
}
//
const handleUserMenuClick = async (data) => {
switch (data.value) {
case 'logout':
await handleLogout()
break
case 'profile':
// TODO:
MessagePlugin.info('功能开发中')
break
case 'change-password':
// TODO:
MessagePlugin.info('功能开发中')
break
}
}
//
const handleLogout = async () => {
try {
await userApi.logout()
} catch (error) {
console.error('登出错误:', error)
} finally {
//
auth.logout()
MessagePlugin.success('已退出登录')
//
router.push('/home')
}
}
//
onMounted(() => {
//
const path = route.path
if (path.startsWith('/')) {
activeMenu.value = path.substring(1) || 'home'
}
})
</script>
<style scoped>
.header {
background-color: #fff;
border-bottom: 1px solid #e7e7e7;
}
.header-container {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 24px;
}
.logo {
display: flex;
align-items: center;
padding: 0 24px;
}
.logo-text {
font-size: 20px;
font-weight: bold;
color: #0052d9;
}
.content {
padding: 24px;
background-color: #f5f5f5;
}
.user-button {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 6px;
transition: background-color 0.2s;
}
.user-button:hover {
background-color: #f0f0f0;
}
.username {
font-size: 14px;
color: #262626;
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
}
</style>

View File

@ -0,0 +1,178 @@
<template>
<t-card class="project-card" hover shadow>
<div class="project-header">
<div class="project-info">
<h3 class="project-name">{{ project.name || '未命名项目' }}</h3>
<p class="project-description">{{ project.description || '暂无描述' }}</p>
</div>
<div class="project-status">
<t-tag
:theme="getStatusTheme(project.status)"
:variant="project.status === 'success' ? 'light' : 'outline'"
>
{{ getStatusText(project.status) }}
</t-tag>
</div>
</div>
<div class="project-details">
<div class="detail-item">
<t-icon name="folder" size="16px" />
<span>{{ project.path || '/' }}</span>
</div>
<div class="detail-item">
<t-icon name="time" size="16px" />
<span>{{ formatTime(project.updatedAt || project.createdAt) }}</span>
</div>
<div class="detail-item" v-if="project.version">
<t-icon name="tag" size="16px" />
<span>v{{ project.version }}</span>
</div>
</div>
<div class="project-actions">
<t-button size="small" variant="outline" @click="$emit('view', project)">
查看详情
</t-button>
<t-button
size="small"
theme="primary"
@click="$emit('deploy', project)"
:loading="deploying"
>
{{ deploying ? '部署中...' : '部署' }}
</t-button>
</div>
</t-card>
</template>
<script setup>
import { ref } from 'vue'
import { formatDistanceToNow } from '@/utils/format'
const props = defineProps({
project: {
type: Object,
required: true
}
})
const emit = defineEmits(['view', 'deploy'])
const deploying = ref(false)
//
const getStatusTheme = (status) => {
const themes = {
success: 'success',
running: 'warning',
failed: 'danger',
pending: 'default'
}
return themes[status] || 'default'
}
//
const getStatusText = (status) => {
const texts = {
success: '部署成功',
running: '部署中',
failed: '部署失败',
pending: '待部署'
}
return texts[status] || '未知状态'
}
//
const formatTime = (time) => {
if (!time) return '未知时间'
try {
return formatDistanceToNow(new Date(time))
} catch (error) {
return '时间格式错误'
}
}
</script>
<style scoped>
.project-card {
height: 100%;
transition: all 0.3s ease;
cursor: pointer;
}
.project-card:hover {
transform: translateY(-2px);
}
.project-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.project-info {
flex: 1;
min-width: 0;
}
.project-name {
font-size: 16px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.project-description {
font-size: 14px;
color: #8c8c8c;
margin: 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.project-status {
flex-shrink: 0;
margin-left: 12px;
}
.project-details {
margin-bottom: 16px;
}
.detail-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-size: 12px;
color: #8c8c8c;
}
.detail-item:last-child {
margin-bottom: 0;
}
.detail-item span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.project-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
:deep(.t-card__body) {
padding: 16px;
}
</style>

View File

@ -1,39 +1,52 @@
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import Login from '@/views/Login.vue'
import Deploy from '@/views/Deploy.vue'
import Files from '@/views/Files.vue'
import Logs from '@/views/Logs.vue'
import Users from '@/views/Users.vue'
import auth from '@/utils/auth'
const routes = [
{
path: '/',
redirect: '/home'
},
{
path: '/login',
name: 'Login',
component: Login,
meta: { requiresAuth: false }
},
{
path: '/home',
name: 'Home',
component: Home
component: Home,
meta: { requiresAuth: false } // 首页不需要登录
},
{
path: '/deploy',
name: 'Deploy',
component: Deploy
component: Deploy,
meta: { requiresAuth: true }
},
{
path: '/files',
name: 'Files',
component: Files
component: Files,
meta: { requiresAuth: true }
},
{
path: '/logs',
name: 'Logs',
component: Logs
component: Logs,
meta: { requiresAuth: true }
},
{
path: '/users',
name: 'Users',
component: Users
component: Users,
meta: { requiresAuth: true }
}
]
@ -42,4 +55,26 @@ const router = createRouter({
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
const isLoggedIn = auth.isLoggedIn()
// 如果路由需要认证
if (to.meta.requiresAuth === true) {
// 未登录,跳转到登录页
if (!isLoggedIn) {
next('/login')
return
}
}
// 已登录用户访问登录页,跳转到首页
if (to.path === '/login' && isLoggedIn) {
next('/home')
return
}
next()
})
export default router

63
src/utils/auth.js Normal file
View File

@ -0,0 +1,63 @@
import { localStorage } from './storage'
// Token 相关常量
const TOKEN_KEY = 'auth_token'
const USER_INFO_KEY = 'user_info'
// 认证工具类
class Auth {
// 设置 token
setToken(token) {
localStorage.set(TOKEN_KEY, token)
}
// 获取 token
getToken() {
return localStorage.get(TOKEN_KEY)
}
// 移除 token
removeToken() {
localStorage.remove(TOKEN_KEY)
}
// 设置用户信息
setUserInfo(userInfo) {
localStorage.set(USER_INFO_KEY, userInfo)
}
// 获取用户信息
getUserInfo() {
return localStorage.get(USER_INFO_KEY)
}
// 移除用户信息
removeUserInfo() {
localStorage.remove(USER_INFO_KEY)
}
// 检查是否已登录
isLoggedIn() {
return !!this.getToken()
}
// 登出
logout() {
this.removeToken()
this.removeUserInfo()
}
// 获取用户权限
getUserPermissions() {
const userInfo = this.getUserInfo()
return userInfo?.permissions || []
}
// 检查用户是否有某个权限
hasPermission(permission) {
const permissions = this.getUserPermissions()
return permissions.includes(permission)
}
}
export default new Auth()

View File

@ -76,6 +76,9 @@ export function formatRelativeTime(date) {
}
}
// 别名函数与ProjectCard组件中的引用保持一致
export const formatDistanceToNow = formatRelativeTime
/**
* 格式化数字
* @param {number} num 数字

View File

@ -1,84 +1,154 @@
<template>
<div class="home">
<t-card class="welcome-card" title="欢迎使用 DeployHelper">
<template #subtitle>
一个简单高效的部署管理系统
</template>
<div class="stats-grid">
<t-card class="stat-card" v-for="stat in stats" :key="stat.title">
<div class="stat-content">
<div class="stat-icon">
<t-icon :name="stat.icon" size="24px" :style="{ color: stat.color }" />
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<div class="header-info">
<h1 class="page-title">项目概览</h1>
<p class="page-subtitle">管理和部署您的项目</p>
</div>
<div class="header-actions" v-if="isLoggedIn">
<t-button theme="primary" @click="goToDeploy">
<t-icon name="add" />
新建项目
</t-button>
</div>
</div>
</div>
<!-- 未登录状态 -->
<div v-if="!isLoggedIn" class="welcome-section">
<t-card class="welcome-card">
<div class="welcome-content">
<h2 class="welcome-title">欢迎使用 DeployHelper</h2>
<p class="welcome-subtitle">一个简单高效的部署管理系统</p>
<div class="welcome-features">
<div class="feature-item">
<t-icon name="rocket" size="24px" color="#0052d9" />
<span>快速部署</span>
</div>
<div class="stat-info">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-title">{{ stat.title }}</div>
<div class="feature-item">
<t-icon name="folder" size="24px" color="#00a870" />
<span>文件管理</span>
</div>
<div class="feature-item">
<t-icon name="chart" size="24px" color="#ed7b2f" />
<span>日志监控</span>
</div>
<div class="feature-item">
<t-icon name="user" size="24px" color="#d54941" />
<span>用户管理</span>
</div>
</div>
</t-card>
</div>
<t-divider />
<t-space direction="vertical" size="large">
<div>
<h3>快速开始</h3>
<t-space>
<t-button theme="primary" @click="goToDeploy">开始部署</t-button>
<t-button variant="outline" @click="goToFiles">管理文件</t-button>
<t-button variant="outline" @click="goToLogs">查看日志</t-button>
</t-space>
<p class="login-hint">登录后查看您的项目</p>
</div>
</t-space>
</t-card>
</t-card>
</div>
<!-- 已登录状态 - 项目列表 -->
<div v-else class="projects-section">
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<t-loading size="large" text="加载项目中..." />
</div>
<!-- 空状态 -->
<div v-else-if="projects.length === 0" class="empty-state">
<t-empty
icon="rocket"
title="还没有项目"
description="创建您的第一个项目开始部署之旅"
>
<t-button theme="primary" @click="goToDeploy">
创建项目
</t-button>
</t-empty>
</div>
<!-- 项目网格 -->
<div v-else class="projects-grid">
<ProjectCard
v-for="project in projects"
:key="project.id"
:project="project"
@view="handleViewProject"
@deploy="handleDeployProject"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { MessagePlugin } from 'tdesign-vue-next'
import ProjectCard from '@/components/ProjectCard.vue'
import { deployApi } from '@/api/deploy'
import auth from '@/utils/auth'
const router = useRouter()
const stats = ref([
{
title: '部署次数',
value: '128',
icon: 'rocket',
color: '#0052d9'
},
{
title: '文件数量',
value: '45',
icon: 'folder',
color: '#00a870'
},
{
title: '活跃用户',
value: '12',
icon: 'user',
color: '#ed7b2f'
},
{
title: '系统状态',
value: '正常',
icon: 'check-circle',
color: '#00a870'
}
])
//
const loading = ref(false)
const projects = ref([])
//
const isLoggedIn = computed(() => auth.isLoggedIn())
//
const fetchProjects = async () => {
if (!isLoggedIn.value) return
loading.value = true
try {
const response = await deployApi.getDeployList({
page: 1,
pageSize: 20
})
if (response.data.code === 0) {
projects.value = response.data.data?.list || []
} else {
MessagePlugin.error(response.data.message || '获取项目列表失败')
}
} catch (error) {
console.error('获取项目列表错误:', error)
MessagePlugin.error('获取项目列表失败,请检查网络连接')
} finally {
loading.value = false
}
}
//
const handleViewProject = (project) => {
router.push(`/deploy/${project.id}`)
}
//
const handleDeployProject = async (project) => {
try {
MessagePlugin.info('部署功能开发中...')
// TODO:
// await deployApi.executeDeploy(project.id)
// MessagePlugin.success('')
} catch (error) {
console.error('部署错误:', error)
MessagePlugin.error('部署失败')
}
}
//
const goToDeploy = () => {
router.push('/deploy')
}
const goToFiles = () => {
router.push('/files')
}
const goToLogs = () => {
router.push('/logs')
}
//
onMounted(() => {
if (isLoggedIn.value) {
fetchProjects()
}
})
</script>
<style scoped>
@ -87,51 +157,159 @@ const goToLogs = () => {
margin: 0 auto;
}
.welcome-card {
/* 页面头部 */
.page-header {
margin-bottom: 24px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
margin: 24px 0;
}
.stat-card {
border: 1px solid #e7e7e7;
}
.stat-content {
.header-content {
display: flex;
align-items: center;
gap: 16px;
justify-content: space-between;
align-items: flex-end;
padding: 0 4px;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 8px;
background-color: #f0f0f0;
.page-title {
font-size: 28px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
}
.page-subtitle {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
.header-actions {
flex-shrink: 0;
}
/* 欢迎页面 */
.welcome-section {
display: flex;
align-items: center;
justify-content: center;
min-height: 50vh;
}
.stat-value {
font-size: 24px;
.welcome-card {
max-width: 800px;
width: 100%;
}
.welcome-content {
text-align: center;
padding: 40px 20px;
}
.welcome-title {
font-size: 28px;
font-weight: bold;
color: #262626;
color: #0052d9;
margin: 0 0 16px 0;
}
.stat-title {
.welcome-subtitle {
font-size: 16px;
color: #8c8c8c;
margin: 0 0 32px 0;
}
.welcome-features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 20px;
margin: 32px 0;
}
.feature-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 20px;
border-radius: 8px;
background-color: #f8f9fa;
transition: transform 0.2s;
}
.feature-item:hover {
transform: translateY(-2px);
}
.feature-item span {
font-size: 14px;
color: #262626;
font-weight: 500;
}
.login-hint {
font-size: 14px;
color: #8c8c8c;
margin-top: 4px;
margin: 24px 0 0 0;
}
h3 {
margin: 0 0 16px 0;
color: #262626;
/* 项目列表 */
.projects-section {
min-height: 400px;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
min-height: 400px;
}
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.page-title {
font-size: 24px;
}
.welcome-features {
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.projects-grid {
grid-template-columns: 1fr;
gap: 16px;
}
}
@media (max-width: 480px) {
.welcome-content {
padding: 24px 16px;
}
.welcome-title {
font-size: 24px;
}
.welcome-features {
grid-template-columns: 1fr;
}
}
</style>

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

@ -0,0 +1,164 @@
<template>
<div class="login-container">
<div class="login-card">
<div class="login-header">
<h1 class="login-title">DeployHelper</h1>
<p class="login-subtitle">部署管理系统</p>
</div>
<t-form
ref="form"
:data="formData"
:rules="rules"
@submit="handleSubmit"
>
<t-form-item name="username">
<t-input
v-model="formData.username"
placeholder="请输入用户名"
size="large"
>
<template #prefix-icon>
<t-icon name="user" />
</template>
</t-input>
</t-form-item>
<t-form-item name="password">
<t-input
v-model="formData.password"
type="password"
placeholder="请输入密码"
size="large"
@keyup.enter="handleSubmit"
>
<template #prefix-icon>
<t-icon name="lock-on" />
</template>
</t-input>
</t-form-item>
<t-form-item>
<t-button
type="submit"
theme="primary"
size="large"
block
:loading="loading"
>
登录
</t-button>
</t-form-item>
</t-form>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { MessagePlugin } from 'tdesign-vue-next'
import { userApi } from '@/api/user'
import auth from '@/utils/auth'
const router = useRouter()
const form = ref()
const loading = ref(false)
const formData = reactive({
username: '',
password: ''
})
const rules = {
username: [
{ required: true, message: '请输入用户名', type: 'error' }
],
password: [
{ required: true, message: '请输入密码', type: 'error' }
]
}
const handleSubmit = async () => {
const valid = await form.value.validate()
if (!valid) return
loading.value = true
try {
const response = await userApi.login(formData)
if (response.data.code === 0) {
const { token, userInfo } = response.data.data
//
auth.setToken(token)
auth.setUserInfo(userInfo)
MessagePlugin.success('登录成功')
//
router.push('/home')
} else {
MessagePlugin.error(response.data.message || '登录失败')
}
} catch (error) {
console.error('登录错误:', error)
MessagePlugin.error('登录失败,请检查网络连接')
} finally {
loading.value = false
}
}
</script>
<style scoped>
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.login-card {
width: 100%;
max-width: 400px;
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.login-header {
text-align: center;
margin-bottom: 32px;
}
.login-title {
font-size: 28px;
font-weight: bold;
color: #0052d9;
margin: 0 0 8px 0;
}
.login-subtitle {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
:deep(.t-form-item) {
margin-bottom: 24px;
}
:deep(.t-input) {
border-radius: 8px;
}
:deep(.t-button) {
border-radius: 8px;
height: 48px;
font-size: 16px;
}
</style>

View File

@ -1,26 +0,0 @@
const { execSync } = require('child_process');
console.log('🧪 开始测试 DeployHelper 前端项目...\n');
try {
// 检查依赖是否安装
console.log('📦 检查依赖...');
execSync('npm list --depth=0', { stdio: 'inherit' });
console.log('✅ 依赖检查通过\n');
// 检查构建
console.log('🔨 测试构建...');
execSync('npm run build', { stdio: 'inherit' });
console.log('✅ 构建测试通过\n');
console.log('🎉 所有测试通过!项目可以正常运行。');
console.log('\n📝 使用说明:');
console.log('1. 开发模式npm run dev');
console.log('2. 构建生产版本npm run build');
console.log('3. 预览构建结果npm run preview');
console.log('\n🌐 访问地址http://localhost:3000');
} catch (error) {
console.error('❌ 测试失败:', error.message);
process.exit(1);
}

View File

@ -11,11 +11,11 @@ export default defineConfig({
}
},
server: {
port: 3000,
port: 8080,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}