feat(frontend): 实现用户登录和项目列表功能
- 新增用户登录页面和相关 API - 实现项目列表展示和基本交互 - 添加全局布局组件和路由守卫 - 优化首页界面,增加未登录状态展示
This commit is contained in:
parent
5fd3dccb3a
commit
d6ba24ae33
63
src/App.vue
63
src/App.vue
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}`)
|
||||
},
|
||||
|
||||
// 执行部署
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 } })
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -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()
|
|
@ -76,6 +76,9 @@ export function formatRelativeTime(date) {
|
|||
}
|
||||
}
|
||||
|
||||
// 别名函数,与ProjectCard组件中的引用保持一致
|
||||
export const formatDistanceToNow = formatRelativeTime
|
||||
|
||||
/**
|
||||
* 格式化数字
|
||||
* @param {number} num 数字
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
|
@ -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/, '')
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue