first commit
This commit is contained in:
commit
5fd3dccb3a
|
@ -0,0 +1,17 @@
|
|||
# API 基础地址
|
||||
VITE_API_BASE_URL=http://localhost:8080
|
||||
|
||||
# 应用标题
|
||||
VITE_APP_TITLE=DeployHelper 管理系统
|
||||
|
||||
# 应用版本
|
||||
VITE_APP_VERSION=1.0.0
|
||||
|
||||
# 是否启用调试模式
|
||||
VITE_DEBUG=false
|
||||
|
||||
# 文件上传大小限制 (MB)
|
||||
VITE_UPLOAD_SIZE_LIMIT=50
|
||||
|
||||
# 分页默认大小
|
||||
VITE_PAGE_SIZE=10
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DeployHelper 前端管理系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "deploy-helper-front",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.12",
|
||||
"vue-router": "^4.5.0",
|
||||
"tdesign-vue-next": "^1.10.4",
|
||||
"tdesign-icons-vue-next": "^0.2.6",
|
||||
"axios": "^1.7.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"vite": "^7.0.6",
|
||||
"@types/node": "^22.10.5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<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>
|
||||
</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}`)
|
||||
}
|
||||
</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;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,39 @@
|
|||
import api from './index'
|
||||
|
||||
// 部署相关 API
|
||||
export const deployApi = {
|
||||
// 获取部署列表
|
||||
getDeployList(params) {
|
||||
return api.get('/sys_deploy_file/list', { params })
|
||||
},
|
||||
|
||||
// 创建部署
|
||||
createDeploy(data) {
|
||||
return api.post('/sys_deploy_file/create', data)
|
||||
},
|
||||
|
||||
// 获取部署详情
|
||||
getDeployDetail(id) {
|
||||
return api.get(`/sys_deploy_file/detail/${id}`)
|
||||
},
|
||||
|
||||
// 更新部署
|
||||
updateDeploy(id, data) {
|
||||
return api.put(`/sys_deploy_file/update/${id}`, data)
|
||||
},
|
||||
|
||||
// 删除部署
|
||||
deleteDeploy(id) {
|
||||
return api.delete(`/sys_deploy_file/delete/${id}`)
|
||||
},
|
||||
|
||||
// 执行部署
|
||||
executeDeploy(id) {
|
||||
return api.post(`/sys_deploy_file/execute/${id}`)
|
||||
},
|
||||
|
||||
// 获取部署状态
|
||||
getDeployStatus(id) {
|
||||
return api.get(`/sys_deploy_file/status/${id}`)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import axios from 'axios'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
|
||||
// 创建 axios 实例
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
// 添加 token
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
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 || '请求失败'))
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// 处理错误响应
|
||||
if (error.response) {
|
||||
const { status, data } = error.response
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
MessagePlugin.error('登录已过期,请重新登录')
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/login'
|
||||
break
|
||||
case 403:
|
||||
MessagePlugin.error('没有权限访问该资源')
|
||||
break
|
||||
case 404:
|
||||
MessagePlugin.error('请求的资源不存在')
|
||||
break
|
||||
case 500:
|
||||
MessagePlugin.error('服务器内部错误')
|
||||
break
|
||||
default:
|
||||
MessagePlugin.error(data.message || '网络错误')
|
||||
}
|
||||
} else if (error.request) {
|
||||
MessagePlugin.error('网络连接失败,请检查网络')
|
||||
} else {
|
||||
MessagePlugin.error(error.message || '请求失败')
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default api
|
|
@ -0,0 +1,42 @@
|
|||
import api from './index'
|
||||
|
||||
// 日志相关 API
|
||||
export const logApi = {
|
||||
// 获取登录日志列表
|
||||
getLoginLogList(params) {
|
||||
return api.get('/sys_login_log/list', { params })
|
||||
},
|
||||
|
||||
// 获取系统日志
|
||||
getSystemLogs(params) {
|
||||
return api.get('/logs/system', { params })
|
||||
},
|
||||
|
||||
// 获取应用日志
|
||||
getAppLogs(params) {
|
||||
return api.get('/logs/app', { params })
|
||||
},
|
||||
|
||||
// 获取错误日志
|
||||
getErrorLogs(params) {
|
||||
return api.get('/logs/error', { params })
|
||||
},
|
||||
|
||||
// 搜索日志
|
||||
searchLogs(params) {
|
||||
return api.get('/logs/search', { params })
|
||||
},
|
||||
|
||||
// 下载日志文件
|
||||
downloadLogs(params) {
|
||||
return api.get('/logs/download', {
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
},
|
||||
|
||||
// 清理日志
|
||||
clearLogs(params) {
|
||||
return api.post('/logs/clear', params)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import api from './index'
|
||||
|
||||
// 文件上传相关 API
|
||||
export const uploadApi = {
|
||||
// 上传文件
|
||||
uploadFile(file, onProgress) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
return api.post('/sys_upload/file', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (onProgress) {
|
||||
const percent = Math.round(
|
||||
(progressEvent.loaded * 100) / progressEvent.total
|
||||
)
|
||||
onProgress(percent)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 获取文件列表
|
||||
getFileList(params) {
|
||||
return api.get('/sys_upload/list', { params })
|
||||
},
|
||||
|
||||
// 删除文件
|
||||
deleteFile(id) {
|
||||
return api.delete(`/sys_upload/delete/${id}`)
|
||||
},
|
||||
|
||||
// 下载文件
|
||||
downloadFile(id) {
|
||||
return api.get(`/sys_upload/download/${id}`, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
},
|
||||
|
||||
// 获取文件详情
|
||||
getFileDetail(id) {
|
||||
return api.get(`/sys_upload/detail/${id}`)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import api from './index'
|
||||
|
||||
// 用户相关 API
|
||||
export const userApi = {
|
||||
// 用户登录
|
||||
login(data) {
|
||||
return api.post('/sys_user/login', data)
|
||||
},
|
||||
|
||||
// 用户登出
|
||||
logout() {
|
||||
return api.post('/sys_user/logout')
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
getUserInfo() {
|
||||
return api.get('/sys_user/info')
|
||||
},
|
||||
|
||||
// 获取用户列表
|
||||
getUserList(params) {
|
||||
return api.get('/sys_user/list', { params })
|
||||
},
|
||||
|
||||
// 创建用户
|
||||
createUser(data) {
|
||||
return api.post('/sys_user/create', data)
|
||||
},
|
||||
|
||||
// 更新用户
|
||||
updateUser(id, data) {
|
||||
return api.put(`/sys_user/update/${id}`, data)
|
||||
},
|
||||
|
||||
// 删除用户
|
||||
deleteUser(id) {
|
||||
return api.delete(`/sys_user/delete/${id}`)
|
||||
},
|
||||
|
||||
// 重置密码
|
||||
resetPassword(id, data) {
|
||||
return api.post(`/sys_user/reset-password/${id}`, data)
|
||||
},
|
||||
|
||||
// 修改密码
|
||||
changePassword(data) {
|
||||
return api.post('/sys_user/change-password', data)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<t-icon :name="icon" size="64px" />
|
||||
</div>
|
||||
<div class="empty-content">
|
||||
<h3 class="empty-title">{{ title }}</h3>
|
||||
<p v-if="description" class="empty-description">{{ description }}</p>
|
||||
<div v-if="$slots.actions" class="empty-actions">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'inbox'
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '暂无数据'
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
margin-bottom: 24px;
|
||||
color: #d9d9d9;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
margin: 0 0 24px 0;
|
||||
font-size: 14px;
|
||||
color: #bfbfbf;
|
||||
max-width: 400px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.empty-actions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div class="loading-spinner" :class="{ 'full-screen': fullScreen }">
|
||||
<div class="spinner-content">
|
||||
<t-loading :size="size" />
|
||||
<p v-if="text" class="loading-text">{{ text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
fullScreen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium'
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.loading-spinner.full-screen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.spinner-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin: 0;
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<div class="page-header">
|
||||
<div class="page-header-content">
|
||||
<div class="page-title">
|
||||
<h1>{{ title }}</h1>
|
||||
<p v-if="description" class="page-description">{{ description }}</p>
|
||||
</div>
|
||||
<div v-if="$slots.extra" class="page-extra">
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showBreadcrumb" class="page-breadcrumb">
|
||||
<t-breadcrumb>
|
||||
<t-breadcrumb-item v-for="item in breadcrumbItems" :key="item.path">
|
||||
<router-link v-if="item.path" :to="item.path">{{ item.title }}</router-link>
|
||||
<span v-else>{{ item.title }}</span>
|
||||
</t-breadcrumb-item>
|
||||
</t-breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showBreadcrumb: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
breadcrumb: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const breadcrumbItems = computed(() => {
|
||||
if (props.breadcrumb.length > 0) {
|
||||
return props.breadcrumb
|
||||
}
|
||||
|
||||
// 根据路由自动生成面包屑
|
||||
const routeMap = {
|
||||
'/home': '首页',
|
||||
'/deploy': '部署管理',
|
||||
'/files': '文件管理',
|
||||
'/logs': '日志管理',
|
||||
'/users': '用户管理'
|
||||
}
|
||||
|
||||
return [
|
||||
{ title: '首页', path: '/home' },
|
||||
{ title: routeMap[route.path] || '页面', path: null }
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.page-header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-title h1 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
margin: 0;
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-extra {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.page-breadcrumb {
|
||||
border-top: 1px solid #e7e7e7;
|
||||
padding-top: 16px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,14 @@
|
|||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
// 引入 TDesign 样式和组件
|
||||
import TDesign from 'tdesign-vue-next'
|
||||
import 'tdesign-vue-next/es/style/index.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(TDesign)
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
|
@ -0,0 +1,45 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Home from '@/views/Home.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'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/home'
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
name: 'Home',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/deploy',
|
||||
name: 'Deploy',
|
||||
component: Deploy
|
||||
},
|
||||
{
|
||||
path: '/files',
|
||||
name: 'Files',
|
||||
component: Files
|
||||
},
|
||||
{
|
||||
path: '/logs',
|
||||
name: 'Logs',
|
||||
component: Logs
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
name: 'Users',
|
||||
component: Users
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
|
@ -0,0 +1,163 @@
|
|||
// 格式化工具函数
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
* @param {number} size 文件大小(字节)
|
||||
* @returns {string} 格式化后的文件大小
|
||||
*/
|
||||
export function formatFileSize(size) {
|
||||
if (!size || size === 0) return '0 B'
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const index = Math.floor(Math.log(size) / Math.log(1024))
|
||||
const formattedSize = (size / Math.pow(1024, index)).toFixed(2)
|
||||
|
||||
return `${formattedSize} ${units[index]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param {Date|string|number} date 时间
|
||||
* @param {string} format 格式模板
|
||||
* @returns {string} 格式化后的时间
|
||||
*/
|
||||
export function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
|
||||
if (!date) return '-'
|
||||
|
||||
const d = new Date(date)
|
||||
if (isNaN(d.getTime())) return '-'
|
||||
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const hours = String(d.getHours()).padStart(2, '0')
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(d.getSeconds()).padStart(2, '0')
|
||||
|
||||
return format
|
||||
.replace('YYYY', year)
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
.replace('ss', seconds)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化相对时间
|
||||
* @param {Date|string|number} date 时间
|
||||
* @returns {string} 相对时间
|
||||
*/
|
||||
export function formatRelativeTime(date) {
|
||||
if (!date) return '-'
|
||||
|
||||
const now = new Date()
|
||||
const targetDate = new Date(date)
|
||||
const diff = now - targetDate
|
||||
|
||||
const minute = 60 * 1000
|
||||
const hour = minute * 60
|
||||
const day = hour * 24
|
||||
const month = day * 30
|
||||
const year = month * 12
|
||||
|
||||
if (diff < minute) {
|
||||
return '刚刚'
|
||||
} else if (diff < hour) {
|
||||
return `${Math.floor(diff / minute)}分钟前`
|
||||
} else if (diff < day) {
|
||||
return `${Math.floor(diff / hour)}小时前`
|
||||
} else if (diff < month) {
|
||||
return `${Math.floor(diff / day)}天前`
|
||||
} else if (diff < year) {
|
||||
return `${Math.floor(diff / month)}个月前`
|
||||
} else {
|
||||
return `${Math.floor(diff / year)}年前`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数字
|
||||
* @param {number} num 数字
|
||||
* @param {number} decimals 小数位数
|
||||
* @returns {string} 格式化后的数字
|
||||
*/
|
||||
export function formatNumber(num, decimals = 0) {
|
||||
if (num === null || num === undefined || isNaN(num)) return '0'
|
||||
|
||||
return Number(num).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化百分比
|
||||
* @param {number} value 数值
|
||||
* @param {number} total 总数
|
||||
* @param {number} decimals 小数位数
|
||||
* @returns {string} 百分比
|
||||
*/
|
||||
export function formatPercent(value, total, decimals = 1) {
|
||||
if (!total || total === 0) return '0%'
|
||||
|
||||
const percent = (value / total) * 100
|
||||
return `${percent.toFixed(decimals)}%`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化状态文本
|
||||
* @param {string} status 状态值
|
||||
* @returns {object} 状态配置
|
||||
*/
|
||||
export function formatStatus(status) {
|
||||
const statusMap = {
|
||||
'active': { text: '启用', theme: 'success' },
|
||||
'inactive': { text: '禁用', theme: 'default' },
|
||||
'pending': { text: '待处理', theme: 'warning' },
|
||||
'processing': { text: '处理中', theme: 'primary' },
|
||||
'success': { text: '成功', theme: 'success' },
|
||||
'failed': { text: '失败', theme: 'danger' },
|
||||
'error': { text: '错误', theme: 'danger' },
|
||||
'cancelled': { text: '已取消', theme: 'default' }
|
||||
}
|
||||
|
||||
return statusMap[status] || { text: status, theme: 'default' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化手机号
|
||||
* @param {string} phone 手机号
|
||||
* @returns {string} 格式化后的手机号
|
||||
*/
|
||||
export function formatPhone(phone) {
|
||||
if (!phone) return '-'
|
||||
|
||||
const phoneStr = String(phone)
|
||||
if (phoneStr.length === 11) {
|
||||
return phoneStr.replace(/(\d{3})(\d{4})(\d{4})/, '$1****$3')
|
||||
}
|
||||
|
||||
return phone
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化邮箱
|
||||
* @param {string} email 邮箱
|
||||
* @returns {string} 格式化后的邮箱
|
||||
*/
|
||||
export function formatEmail(email) {
|
||||
if (!email) return '-'
|
||||
|
||||
const atIndex = email.indexOf('@')
|
||||
if (atIndex > 0) {
|
||||
const username = email.substring(0, atIndex)
|
||||
const domain = email.substring(atIndex)
|
||||
|
||||
if (username.length > 3) {
|
||||
return `${username.substring(0, 3)}***${domain}`
|
||||
}
|
||||
}
|
||||
|
||||
return email
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
// 本地存储工具类
|
||||
class Storage {
|
||||
constructor(storage = localStorage) {
|
||||
this.storage = storage
|
||||
}
|
||||
|
||||
// 设置存储项
|
||||
set(key, value, expire = null) {
|
||||
const item = {
|
||||
value,
|
||||
expire: expire ? Date.now() + expire * 1000 : null
|
||||
}
|
||||
this.storage.setItem(key, JSON.stringify(item))
|
||||
}
|
||||
|
||||
// 获取存储项
|
||||
get(key, defaultValue = null) {
|
||||
try {
|
||||
const item = this.storage.getItem(key)
|
||||
if (!item) return defaultValue
|
||||
|
||||
const data = JSON.parse(item)
|
||||
|
||||
// 检查是否过期
|
||||
if (data.expire && Date.now() > data.expire) {
|
||||
this.remove(key)
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return data.value
|
||||
} catch (error) {
|
||||
console.error('Storage get error:', error)
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// 删除存储项
|
||||
remove(key) {
|
||||
this.storage.removeItem(key)
|
||||
}
|
||||
|
||||
// 清空存储
|
||||
clear() {
|
||||
this.storage.clear()
|
||||
}
|
||||
|
||||
// 获取所有键
|
||||
keys() {
|
||||
return Object.keys(this.storage)
|
||||
}
|
||||
|
||||
// 检查键是否存在
|
||||
has(key) {
|
||||
return this.storage.getItem(key) !== null
|
||||
}
|
||||
}
|
||||
|
||||
// 创建实例
|
||||
export const localStorage = new Storage(window.localStorage)
|
||||
export const sessionStorage = new Storage(window.sessionStorage)
|
||||
|
||||
export default localStorage
|
|
@ -0,0 +1,190 @@
|
|||
<template>
|
||||
<div class="deploy">
|
||||
<t-card title="部署管理">
|
||||
<template #actions>
|
||||
<t-button theme="primary" @click="handleCreate">
|
||||
<t-icon name="add" />
|
||||
创建部署
|
||||
</t-button>
|
||||
</template>
|
||||
|
||||
<t-table
|
||||
:data="deployList"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
:pagination="pagination"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</t-card>
|
||||
|
||||
<!-- 创建部署对话框 -->
|
||||
<t-dialog
|
||||
v-model:visible="createVisible"
|
||||
title="创建部署"
|
||||
width="600px"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<t-form ref="formRef" :data="formData" :rules="rules" label-width="100px">
|
||||
<t-form-item label="部署名称" name="name">
|
||||
<t-input v-model="formData.name" placeholder="请输入部署名称" />
|
||||
</t-form-item>
|
||||
<t-form-item label="部署环境" name="environment">
|
||||
<t-select v-model="formData.environment" placeholder="请选择部署环境">
|
||||
<t-option value="dev" label="开发环境" />
|
||||
<t-option value="test" label="测试环境" />
|
||||
<t-option value="prod" label="生产环境" />
|
||||
</t-select>
|
||||
</t-form-item>
|
||||
<t-form-item label="部署文件" name="file">
|
||||
<t-upload
|
||||
v-model="formData.file"
|
||||
theme="file-input"
|
||||
placeholder="请选择部署文件"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="描述" name="description">
|
||||
<t-textarea
|
||||
v-model="formData.description"
|
||||
placeholder="请输入部署描述"
|
||||
:maxlength="200"
|
||||
/>
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</t-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
|
||||
const loading = ref(false)
|
||||
const createVisible = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
const deployList = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '前端应用部署',
|
||||
environment: '生产环境',
|
||||
status: '成功',
|
||||
createTime: '2024-01-15 10:30:00',
|
||||
user: 'admin'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '后端API部署',
|
||||
environment: '测试环境',
|
||||
status: '进行中',
|
||||
createTime: '2024-01-15 09:20:00',
|
||||
user: 'developer'
|
||||
}
|
||||
])
|
||||
|
||||
const columns = [
|
||||
{
|
||||
colKey: 'id',
|
||||
title: 'ID',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
colKey: 'name',
|
||||
title: '部署名称'
|
||||
},
|
||||
{
|
||||
colKey: 'environment',
|
||||
title: '环境'
|
||||
},
|
||||
{
|
||||
colKey: 'status',
|
||||
title: '状态',
|
||||
cell: ({ row }) => {
|
||||
if (!row || !row.status) return '-'
|
||||
|
||||
const statusMap = {
|
||||
'成功': 'success',
|
||||
'失败': 'danger',
|
||||
'进行中': 'warning'
|
||||
}
|
||||
return `<t-tag theme="${statusMap[row.status] || 'default'}">${row.status}</t-tag>`
|
||||
}
|
||||
},
|
||||
{
|
||||
colKey: 'user',
|
||||
title: '操作用户'
|
||||
},
|
||||
{
|
||||
colKey: 'createTime',
|
||||
title: '创建时间'
|
||||
},
|
||||
{
|
||||
colKey: 'action',
|
||||
title: '操作',
|
||||
cell: ({ row }) => {
|
||||
if (!row || !row.id) return '-'
|
||||
return `
|
||||
<t-button theme="primary" variant="text" size="small">查看</t-button>
|
||||
<t-button theme="danger" variant="text" size="small">删除</t-button>
|
||||
`
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 2,
|
||||
showJumper: true,
|
||||
showSizer: true
|
||||
})
|
||||
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
environment: '',
|
||||
file: [],
|
||||
description: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入部署名称' }],
|
||||
environment: [{ required: true, message: '请选择部署环境' }],
|
||||
file: [{ required: true, message: '请选择部署文件' }]
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
createVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const result = await formRef.value.validate()
|
||||
if (result === true) {
|
||||
MessagePlugin.success('部署创建成功')
|
||||
createVisible.value = false
|
||||
// 重置表单
|
||||
Object.keys(formData).forEach(key => {
|
||||
if (Array.isArray(formData[key])) {
|
||||
formData[key] = []
|
||||
} else {
|
||||
formData[key] = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handlePageChange = (pageInfo) => {
|
||||
pagination.current = pageInfo.current
|
||||
pagination.pageSize = pageInfo.pageSize
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化数据
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.deploy {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,231 @@
|
|||
<template>
|
||||
<div class="files">
|
||||
<t-card title="文件管理">
|
||||
<template #actions>
|
||||
<t-space>
|
||||
<t-button theme="primary" @click="handleUpload">
|
||||
<t-icon name="upload" />
|
||||
上传文件
|
||||
</t-button>
|
||||
<t-button variant="outline" @click="handleRefresh">
|
||||
<t-icon name="refresh" />
|
||||
刷新
|
||||
</t-button>
|
||||
</t-space>
|
||||
</template>
|
||||
|
||||
<t-table
|
||||
:data="fileList"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
:pagination="pagination"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</t-card>
|
||||
|
||||
<!-- 上传文件对话框 -->
|
||||
<t-dialog
|
||||
v-model:visible="uploadVisible"
|
||||
title="上传文件"
|
||||
width="600px"
|
||||
@confirm="handleUploadSubmit"
|
||||
>
|
||||
<t-upload
|
||||
v-model="uploadFiles"
|
||||
theme="file-flow"
|
||||
:auto-upload="false"
|
||||
multiple
|
||||
:max="5"
|
||||
accept=".zip,.tar,.gz,.jar,.war"
|
||||
>
|
||||
<t-upload-dragger>
|
||||
<div class="upload-dragger-content">
|
||||
<t-icon name="cloud-upload" size="48px" />
|
||||
<div class="upload-text">
|
||||
<div class="upload-tip">点击上传或将文件拖拽到此区域</div>
|
||||
<div class="upload-sub-tip">支持 .zip、.tar、.gz、.jar、.war 格式</div>
|
||||
</div>
|
||||
</div>
|
||||
</t-upload-dragger>
|
||||
</t-upload>
|
||||
</t-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
|
||||
const loading = ref(false)
|
||||
const uploadVisible = ref(false)
|
||||
const uploadFiles = ref([])
|
||||
|
||||
const fileList = ref([
|
||||
{
|
||||
id: 1,
|
||||
filename: 'app-v1.2.0.zip',
|
||||
size: '15.2 MB',
|
||||
type: 'ZIP',
|
||||
uploadTime: '2024-01-15 10:30:00',
|
||||
uploader: 'admin',
|
||||
status: '已上传'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
filename: 'backend-api.jar',
|
||||
size: '32.8 MB',
|
||||
type: 'JAR',
|
||||
uploadTime: '2024-01-15 09:20:00',
|
||||
uploader: 'developer',
|
||||
status: '已上传'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
filename: 'config.tar.gz',
|
||||
size: '2.1 MB',
|
||||
type: 'TAR.GZ',
|
||||
uploadTime: '2024-01-14 16:45:00',
|
||||
uploader: 'admin',
|
||||
status: '已上传'
|
||||
}
|
||||
])
|
||||
|
||||
const columns = [
|
||||
{
|
||||
colKey: 'id',
|
||||
title: 'ID',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
colKey: 'filename',
|
||||
title: '文件名',
|
||||
cell: ({ row }) => {
|
||||
if (!row || !row.filename) return '-'
|
||||
|
||||
return `<div class="file-cell">
|
||||
<t-icon name="file" style="margin-right: 8px;" />
|
||||
${row.filename}
|
||||
</div>`
|
||||
}
|
||||
},
|
||||
{
|
||||
colKey: 'size',
|
||||
title: '文件大小'
|
||||
},
|
||||
{
|
||||
colKey: 'type',
|
||||
title: '文件类型',
|
||||
cell: ({ row }) => {
|
||||
if (!row || !row.type) return '-'
|
||||
|
||||
const typeMap = {
|
||||
'ZIP': 'primary',
|
||||
'JAR': 'success',
|
||||
'TAR.GZ': 'warning',
|
||||
'WAR': 'danger'
|
||||
}
|
||||
return `<t-tag theme="${typeMap[row.type] || 'default'}">${row.type}</t-tag>`
|
||||
}
|
||||
},
|
||||
{
|
||||
colKey: 'uploader',
|
||||
title: '上传者'
|
||||
},
|
||||
{
|
||||
colKey: 'uploadTime',
|
||||
title: '上传时间'
|
||||
},
|
||||
{
|
||||
colKey: 'status',
|
||||
title: '状态',
|
||||
cell: ({ row }) => {
|
||||
if (!row || !row.status) return '-'
|
||||
return `<t-tag theme="success">${row.status}</t-tag>`
|
||||
}
|
||||
},
|
||||
{
|
||||
colKey: 'action',
|
||||
title: '操作',
|
||||
cell: ({ row }) => {
|
||||
if (!row || !row.id) return '-'
|
||||
return `
|
||||
<t-button theme="primary" variant="text" size="small">下载</t-button>
|
||||
<t-button theme="danger" variant="text" size="small">删除</t-button>
|
||||
`
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 3,
|
||||
showJumper: true,
|
||||
showSizer: true
|
||||
})
|
||||
|
||||
const handleUpload = () => {
|
||||
uploadVisible.value = true
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
MessagePlugin.success('刷新成功')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleUploadSubmit = () => {
|
||||
if (uploadFiles.value.length === 0) {
|
||||
MessagePlugin.warning('请选择要上传的文件')
|
||||
return
|
||||
}
|
||||
|
||||
MessagePlugin.success('文件上传成功')
|
||||
uploadVisible.value = false
|
||||
uploadFiles.value = []
|
||||
}
|
||||
|
||||
const handlePageChange = (pageInfo) => {
|
||||
pagination.current = pageInfo.current
|
||||
pagination.pageSize = pageInfo.pageSize
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化数据
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.files {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.file-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.upload-dragger-content {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
font-size: 16px;
|
||||
color: #262626;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-sub-tip {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,137 @@
|
|||
<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>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
<div class="stat-title">{{ stat.title }}</div>
|
||||
</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>
|
||||
</div>
|
||||
</t-space>
|
||||
</t-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
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 goToDeploy = () => {
|
||||
router.push('/deploy')
|
||||
}
|
||||
|
||||
const goToFiles = () => {
|
||||
router.push('/files')
|
||||
}
|
||||
|
||||
const goToLogs = () => {
|
||||
router.push('/logs')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.welcome-card {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 16px 0;
|
||||
color: #262626;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,252 @@
|
|||
<template>
|
||||
<div class="logs">
|
||||
<t-card title="日志管理">
|
||||
<template #actions>
|
||||
<t-space>
|
||||
<t-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索日志内容"
|
||||
style="width: 200px;"
|
||||
@enter="handleSearch"
|
||||
>
|
||||
<template #suffix-icon>
|
||||
<t-icon name="search" />
|
||||
</template>
|
||||
</t-input>
|
||||
<t-select
|
||||
v-model="logLevel"
|
||||
placeholder="日志级别"
|
||||
style="width: 120px;"
|
||||
@change="handleFilter"
|
||||
>
|
||||
<t-option value="" label="全部" />
|
||||
<t-option value="INFO" label="INFO" />
|
||||
<t-option value="WARN" label="WARN" />
|
||||
<t-option value="ERROR" label="ERROR" />
|
||||
<t-option value="DEBUG" label="DEBUG" />
|
||||
</t-select>
|
||||
<t-button variant="outline" @click="handleRefresh">
|
||||
<t-icon name="refresh" />
|
||||
刷新
|
||||
</t-button>
|
||||
</t-space>
|
||||
</template>
|
||||
|
||||
<t-table
|
||||
:data="logList"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
:pagination="pagination"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</t-card>
|
||||
|
||||
<!-- 日志详情对话框 -->
|
||||
<t-dialog
|
||||
v-model:visible="detailVisible"
|
||||
title="日志详情"
|
||||
width="800px"
|
||||
:footer="false"
|
||||
>
|
||||
<div class="log-detail" v-if="currentLog">
|
||||
<t-descriptions :data="logDetailData" />
|
||||
<t-divider />
|
||||
<div class="log-content">
|
||||
<h4>日志内容</h4>
|
||||
<pre class="log-text">{{ currentLog.content }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</t-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
|
||||
const loading = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const logLevel = ref('')
|
||||
const currentLog = ref(null)
|
||||
|
||||
const logList = ref([
|
||||
{
|
||||
id: 1,
|
||||
level: 'INFO',
|
||||
module: '部署模块',
|
||||
message: '部署任务开始执行',
|
||||
content: '[2024-01-15 10:30:00] INFO: Deploy task started for project: frontend-app\nTarget environment: production\nExecuting deployment steps...',
|
||||
timestamp: '2024-01-15 10:30:00',
|
||||
user: 'admin'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
level: 'ERROR',
|
||||
module: '文件上传',
|
||||
message: '文件上传失败:文件大小超过限制',
|
||||
content: '[2024-01-15 10:25:00] ERROR: File upload failed\nReason: File size exceeds maximum limit (50MB)\nFile: large-package.zip (85MB)\nUser: developer',
|
||||
timestamp: '2024-01-15 10:25:00',
|
||||
user: 'developer'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
level: 'WARN',
|
||||
module: '系统监控',
|
||||
message: '内存使用率较高',
|
||||
content: '[2024-01-15 10:20:00] WARN: Memory usage is high\nCurrent usage: 85%\nRecommend cleanup or scaling\nSystem performance may be affected',
|
||||
timestamp: '2024-01-15 10:20:00',
|
||||
user: 'system'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
level: 'INFO',
|
||||
module: '用户登录',
|
||||
message: '用户登录成功',
|
||||
content: '[2024-01-15 10:15:00] INFO: User login successful\nUsername: admin\nIP Address: 192.168.1.100\nUser Agent: Mozilla/5.0...',
|
||||
timestamp: '2024-01-15 10:15:00',
|
||||
user: 'admin'
|
||||
}
|
||||
])
|
||||
|
||||
const columns = [
|
||||
{
|
||||
colKey: 'id',
|
||||
title: 'ID',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
colKey: 'level',
|
||||
title: '级别',
|
||||
width: 100,
|
||||
cell: ({ row }) => {
|
||||
if (!row || !row.level) return '-'
|
||||
|
||||
const levelMap = {
|
||||
'INFO': { theme: 'primary', color: '#0052d9' },
|
||||
'WARN': { theme: 'warning', color: '#ed7b2f' },
|
||||
'ERROR': { theme: 'danger', color: '#e34d59' },
|
||||
'DEBUG': { theme: 'default', color: '#8c8c8c' }
|
||||
}
|
||||
const config = levelMap[row.level] || levelMap['DEBUG']
|
||||
return `<t-tag theme="${config.theme}">${row.level}</t-tag>`
|
||||
}
|
||||
},
|
||||
{
|
||||
colKey: 'module',
|
||||
title: '模块'
|
||||
},
|
||||
{
|
||||
colKey: 'message',
|
||||
title: '消息',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
colKey: 'user',
|
||||
title: '用户'
|
||||
},
|
||||
{
|
||||
colKey: 'timestamp',
|
||||
title: '时间'
|
||||
},
|
||||
{
|
||||
colKey: 'action',
|
||||
title: '操作',
|
||||
cell: ({ row }) => {
|
||||
if (!row || !row.id) return '-'
|
||||
return `<t-button theme="primary" variant="text" size="small" onclick="showDetail(${row.id})">查看详情</t-button>`
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 4,
|
||||
showJumper: true,
|
||||
showSizer: true
|
||||
})
|
||||
|
||||
const logDetailData = computed(() => {
|
||||
if (!currentLog.value) return []
|
||||
|
||||
return [
|
||||
{ label: '日志ID', value: currentLog.value.id },
|
||||
{ label: '级别', value: currentLog.value.level },
|
||||
{ label: '模块', value: currentLog.value.module },
|
||||
{ label: '用户', value: currentLog.value.user },
|
||||
{ label: '时间', value: currentLog.value.timestamp },
|
||||
{ label: '消息', value: currentLog.value.message }
|
||||
]
|
||||
})
|
||||
|
||||
// 全局函数,供表格调用
|
||||
window.showDetail = (id) => {
|
||||
currentLog.value = logList.value.find(log => log.id === id)
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
MessagePlugin.info(`搜索关键词:${searchKeyword.value}`)
|
||||
// 实际项目中这里会调用 API 进行搜索
|
||||
}
|
||||
|
||||
const handleFilter = () => {
|
||||
MessagePlugin.info(`筛选日志级别:${logLevel.value || '全部'}`)
|
||||
// 实际项目中这里会调用 API 进行筛选
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
MessagePlugin.success('刷新成功')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handlePageChange = (pageInfo) => {
|
||||
pagination.current = pageInfo.current
|
||||
pagination.pageSize = pageInfo.pageSize
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化数据
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logs {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.log-detail {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.log-content h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.log-text {
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
margin: 0;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,384 @@
|
|||
<template>
|
||||
<div class="users">
|
||||
<t-card title="用户管理">
|
||||
<template #actions>
|
||||
<t-space>
|
||||
<t-button theme="primary" @click="handleCreate">
|
||||
<t-icon name="user-add" />
|
||||
添加用户
|
||||
</t-button>
|
||||
<t-button variant="outline" @click="handleRefresh">
|
||||
<t-icon name="refresh" />
|
||||
刷新
|
||||
</t-button>
|
||||
</t-space>
|
||||
</template>
|
||||
|
||||
<t-table
|
||||
:data="userList"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
:pagination="pagination"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</t-card>
|
||||
|
||||
<!-- 添加/编辑用户对话框 -->
|
||||
<t-dialog
|
||||
v-model:visible="formVisible"
|
||||
:title="isEdit ? '编辑用户' : '添加用户'"
|
||||
width="600px"
|
||||
@confirm="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<t-form ref="formRef" :data="formData" :rules="rules" label-width="100px">
|
||||
<t-form-item label="用户名" name="username">
|
||||
<t-input
|
||||
v-model="formData.username"
|
||||
placeholder="请输入用户名"
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="真实姓名" name="realName">
|
||||
<t-input v-model="formData.realName" placeholder="请输入真实姓名" />
|
||||
</t-form-item>
|
||||
<t-form-item label="邮箱" name="email">
|
||||
<t-input v-model="formData.email" placeholder="请输入邮箱地址" />
|
||||
</t-form-item>
|
||||
<t-form-item label="手机号" name="phone">
|
||||
<t-input v-model="formData.phone" placeholder="请输入手机号" />
|
||||
</t-form-item>
|
||||
<t-form-item label="角色" name="role">
|
||||
<t-select v-model="formData.role" placeholder="请选择用户角色">
|
||||
<t-option value="admin" label="管理员" />
|
||||
<t-option value="user" label="普通用户" />
|
||||
<t-option value="viewer" label="只读用户" />
|
||||
</t-select>
|
||||
</t-form-item>
|
||||
<t-form-item label="状态" name="status">
|
||||
<t-radio-group v-model="formData.status">
|
||||
<t-radio value="active">启用</t-radio>
|
||||
<t-radio value="inactive">禁用</t-radio>
|
||||
</t-radio-group>
|
||||
</t-form-item>
|
||||
<t-form-item v-if="!isEdit" label="密码" name="password">
|
||||
<t-input
|
||||
v-model="formData.password"
|
||||
type="password"
|
||||
placeholder="请输入初始密码"
|
||||
/>
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</t-dialog>
|
||||
|
||||
<!-- 重置密码对话框 -->
|
||||
<t-dialog
|
||||
v-model:visible="resetPasswordVisible"
|
||||
title="重置密码"
|
||||
width="400px"
|
||||
@confirm="handleResetPassword"
|
||||
>
|
||||
<t-form ref="passwordFormRef" :data="passwordData" :rules="passwordRules" label-width="100px">
|
||||
<t-form-item label="新密码" name="newPassword">
|
||||
<t-input
|
||||
v-model="passwordData.newPassword"
|
||||
type="password"
|
||||
placeholder="请输入新密码"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="确认密码" name="confirmPassword">
|
||||
<t-input
|
||||
v-model="passwordData.confirmPassword"
|
||||
type="password"
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</t-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
|
||||
|
||||
const loading = ref(false)
|
||||
const formVisible = ref(false)
|
||||
const resetPasswordVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const currentUser = ref(null)
|
||||
const formRef = ref()
|
||||
const passwordFormRef = ref()
|
||||
|
||||
const userList = ref([
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
realName: '系统管理员',
|
||||
email: 'admin@example.com',
|
||||
phone: '138****8888',
|
||||
role: 'admin',
|
||||
status: 'active',
|
||||
lastLoginTime: '2024-01-15 10:30:00',
|
||||
createTime: '2024-01-01 00:00:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'developer',
|
||||
realName: '开发者',
|
||||
email: 'dev@example.com',
|
||||
phone: '139****9999',
|
||||
role: 'user',
|
||||
status: 'active',
|
||||
lastLoginTime: '2024-01-15 09:20:00',
|
||||
createTime: '2024-01-10 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: 'tester',
|
||||
realName: '测试员',
|
||||
email: 'test@example.com',
|
||||
phone: '136****6666',
|
||||
role: 'viewer',
|
||||
status: 'inactive',
|
||||
lastLoginTime: '2024-01-14 16:45:00',
|
||||
createTime: '2024-01-12 14:30:00'
|
||||
}
|
||||
])
|
||||
|
||||
const columns = [
|
||||
{
|
||||
colKey: 'id',
|
||||
title: 'ID',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
colKey: 'username',
|
||||
title: '用户名'
|
||||
},
|
||||
{
|
||||
colKey: 'realName',
|
||||
title: '真实姓名'
|
||||
},
|
||||
{
|
||||
colKey: 'email',
|
||||
title: '邮箱'
|
||||
},
|
||||
{
|
||||
colKey: 'phone',
|
||||
title: '手机号'
|
||||
},
|
||||
{
|
||||
colKey: 'role',
|
||||
title: '角色',
|
||||
cell: ({ row }) => {
|
||||
if (!row || !row.role) return '-'
|
||||
|
||||
const roleMap = {
|
||||
'admin': { label: '管理员', theme: 'danger' },
|
||||
'user': { label: '普通用户', theme: 'primary' },
|
||||
'viewer': { label: '只读用户', theme: 'default' }
|
||||
}
|
||||
const config = roleMap[row.role] || roleMap['viewer']
|
||||
return `<t-tag theme="${config.theme}">${config.label}</t-tag>`
|
||||
}
|
||||
},
|
||||
{
|
||||
colKey: 'status',
|
||||
title: '状态',
|
||||
cell: ({ row }) => {
|
||||
if (!row || !row.status) return '-'
|
||||
|
||||
return row.status === 'active'
|
||||
? '<t-tag theme="success">启用</t-tag>'
|
||||
: '<t-tag theme="default">禁用</t-tag>'
|
||||
}
|
||||
},
|
||||
{
|
||||
colKey: 'lastLoginTime',
|
||||
title: '最后登录'
|
||||
},
|
||||
{
|
||||
colKey: 'action',
|
||||
title: '操作',
|
||||
cell: ({ row }) => {
|
||||
if (!row || !row.id) return '-'
|
||||
|
||||
return `
|
||||
<t-button theme="primary" variant="text" size="small" onclick="editUser(${row.id})">编辑</t-button>
|
||||
<t-button theme="warning" variant="text" size="small" onclick="resetUserPassword(${row.id})">重置密码</t-button>
|
||||
<t-button theme="danger" variant="text" size="small" onclick="deleteUser(${row.id})">删除</t-button>
|
||||
`
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 3,
|
||||
showJumper: true,
|
||||
showSizer: true
|
||||
})
|
||||
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
realName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
role: '',
|
||||
status: 'active',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const passwordData = reactive({
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
username: [{ required: true, message: '请输入用户名' }],
|
||||
realName: [{ required: true, message: '请输入真实姓名' }],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱地址' },
|
||||
{ email: true, message: '请输入正确的邮箱格式' }
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式' }
|
||||
],
|
||||
role: [{ required: true, message: '请选择用户角色' }],
|
||||
password: [{ required: true, message: '请输入初始密码' }]
|
||||
}
|
||||
|
||||
const passwordRules = {
|
||||
newPassword: [
|
||||
{ required: true, message: '请输入新密码' },
|
||||
{ min: 6, message: '密码长度不能少于6位' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请再次输入新密码' },
|
||||
{
|
||||
validator: (val) => val === passwordData.newPassword,
|
||||
message: '两次输入的密码不一致'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 全局函数,供表格调用
|
||||
window.editUser = (id) => {
|
||||
currentUser.value = userList.value.find(user => user.id === id)
|
||||
if (currentUser.value) {
|
||||
Object.keys(formData).forEach(key => {
|
||||
if (key !== 'password' && currentUser.value[key] !== undefined) {
|
||||
formData[key] = currentUser.value[key]
|
||||
}
|
||||
})
|
||||
isEdit.value = true
|
||||
formVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
window.resetUserPassword = (id) => {
|
||||
currentUser.value = userList.value.find(user => user.id === id)
|
||||
if (currentUser.value) {
|
||||
passwordData.newPassword = ''
|
||||
passwordData.confirmPassword = ''
|
||||
resetPasswordVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
window.deleteUser = (id) => {
|
||||
const user = userList.value.find(u => u.id === id)
|
||||
if (!user) return
|
||||
|
||||
if (user.username === 'admin') {
|
||||
MessagePlugin.warning('不能删除管理员账户')
|
||||
return
|
||||
}
|
||||
|
||||
DialogPlugin.confirm({
|
||||
header: '确认删除',
|
||||
body: `确定要删除用户 "${user.realName}" 吗?此操作不可恢复。`,
|
||||
onConfirm: () => {
|
||||
const index = userList.value.findIndex(u => u.id === id)
|
||||
if (index > -1) {
|
||||
userList.value.splice(index, 1)
|
||||
pagination.total--
|
||||
MessagePlugin.success('用户删除成功')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
Object.keys(formData).forEach(key => {
|
||||
formData[key] = key === 'status' ? 'active' : ''
|
||||
})
|
||||
isEdit.value = false
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
MessagePlugin.success('刷新成功')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const result = await formRef.value.validate()
|
||||
if (result === true) {
|
||||
if (isEdit.value) {
|
||||
// 更新用户
|
||||
const index = userList.value.findIndex(u => u.id === currentUser.value.id)
|
||||
if (index > -1) {
|
||||
Object.assign(userList.value[index], formData)
|
||||
MessagePlugin.success('用户更新成功')
|
||||
}
|
||||
} else {
|
||||
// 添加用户
|
||||
const newUser = {
|
||||
id: Date.now(),
|
||||
...formData,
|
||||
lastLoginTime: '-',
|
||||
createTime: new Date().toLocaleString('zh-CN')
|
||||
}
|
||||
userList.value.push(newUser)
|
||||
pagination.total++
|
||||
MessagePlugin.success('用户添加成功')
|
||||
}
|
||||
formVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
formVisible.value = false
|
||||
}
|
||||
|
||||
const handleResetPassword = async () => {
|
||||
const result = await passwordFormRef.value.validate()
|
||||
if (result === true) {
|
||||
MessagePlugin.success(`用户 "${currentUser.value.realName}" 密码重置成功`)
|
||||
resetPasswordVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handlePageChange = (pageInfo) => {
|
||||
pagination.current = pageInfo.current
|
||||
pagination.pageSize = pageInfo.pageSize
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化数据
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.users {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,26 @@
|
|||
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);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist'
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue