把原来bootstrap写的那几个页面页用Vue重写了一遍 对手机端进行页面优化
This commit is contained in:
parent
e402340dc9
commit
d66c6945d1
16 changed files with 1447 additions and 65 deletions
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
|
|
|
@ -14,13 +14,13 @@ const request = axios.create({
|
|||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
config => {
|
||||
// 如果不是登录请求,就添加token
|
||||
if (!config.url.includes('/user/login') && !config.url.includes('/user/register')) {
|
||||
const token = JSON.parse(localStorage.getItem('token')).token
|
||||
// 如果是访客页面的请求,不需要添加 token
|
||||
if (!config.url.startsWith('/visitor')) {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
const tokenData = JSON.parse(token)
|
||||
config.headers.Authorization = `Bearer ${tokenData.token}`
|
||||
}
|
||||
console.log('Request Headers:', config.headers.Authorization)
|
||||
}
|
||||
return config
|
||||
},
|
||||
|
@ -38,11 +38,9 @@ request.interceptors.response.use(
|
|||
if (data.retcode === 0) {
|
||||
return data
|
||||
}
|
||||
// 显示错误消息对话框
|
||||
ElMessageBox.alert(data.message || '操作失败', '错误', {
|
||||
type: 'error',
|
||||
confirmButtonText: '确定'
|
||||
})
|
||||
|
||||
// 显示错误消息
|
||||
ElMessage.error(data.message || '操作失败')
|
||||
return Promise.reject(new Error(data.message || '操作失败'))
|
||||
},
|
||||
error => {
|
||||
|
@ -51,9 +49,12 @@ request.interceptors.response.use(
|
|||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userRole')
|
||||
router.push('/login')
|
||||
// 只有非访客页面才需要清除登录状态并跳转
|
||||
if (!router.currentRoute.value.path.startsWith('/visitor')) {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userRole')
|
||||
router.push('/login')
|
||||
}
|
||||
break
|
||||
case 403:
|
||||
ElMessage.error('没有权限访问')
|
||||
|
@ -62,10 +63,8 @@ request.interceptors.response.use(
|
|||
ElMessage.error(error.response.data?.message || '请求失败')
|
||||
}
|
||||
} else if (error.request) {
|
||||
// 请求已发出但没有收到响应
|
||||
ElMessage.error('服务器无响应,请检查后端服务是否启动')
|
||||
} else {
|
||||
// 请求配置出错
|
||||
ElMessage.error('请求配置错误')
|
||||
}
|
||||
return Promise.reject(error)
|
||||
|
|
49
src/api/visitor.js
Normal file
49
src/api/visitor.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
import request from './request'
|
||||
|
||||
export const VisitorAPI = {
|
||||
/**
|
||||
* 获取服务器负载信息
|
||||
*/
|
||||
getServerLoad() {
|
||||
return request.get('/GetServerLoad', {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加离线下载任务
|
||||
* @param {string} url 下载链接
|
||||
*/
|
||||
addOfflineTask(url) {
|
||||
return request.post('/AddOfflineTask', null, {
|
||||
params: { url },
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取我的任务列表
|
||||
*/
|
||||
getMyTasks() {
|
||||
return request.get('/GetMyTasks', {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除访客信息
|
||||
*/
|
||||
deleteVisitorInfo() {
|
||||
return request.delete('/VisitorManagement/DeleteInfo', {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<el-container class="layout-container">
|
||||
<el-aside width="220px" class="aside">
|
||||
<el-aside :width="isCollapse ? '64px' : '220px'" class="aside">
|
||||
<div class="logo">
|
||||
<img src="../assets/logo.png" alt="logo" class="logo-img">
|
||||
<span class="logo-text">iFileProxy<br />数据管理系统</span>
|
||||
<span class="logo-text" v-show="!isCollapse">iFileProxy<br />数据管理系统</span>
|
||||
</div>
|
||||
<el-menu
|
||||
router
|
||||
|
@ -11,7 +11,8 @@
|
|||
class="el-menu-vertical"
|
||||
background-color="#001529"
|
||||
text-color="rgba(255,255,255,0.65)"
|
||||
active-text-color="#fff">
|
||||
active-text-color="#fff"
|
||||
:collapse="isCollapse">
|
||||
<el-menu-item index="/dashboard">
|
||||
<el-icon><DataLine /></el-icon>
|
||||
<span>概览面板</span>
|
||||
|
@ -42,7 +43,11 @@
|
|||
<el-container>
|
||||
<el-header class="header">
|
||||
<div class="header-left">
|
||||
<el-icon class="toggle-icon"><Expand /></el-icon>
|
||||
<el-icon
|
||||
class="toggle-icon"
|
||||
@click="toggleSidebar">
|
||||
<component :is="isCollapse ? 'Expand' : 'Fold'" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-dropdown>
|
||||
|
@ -73,9 +78,9 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { DataLine, List, Expand, Setting, User, SwitchButton } from '@element-plus/icons-vue'
|
||||
import { DataLine, List, Expand, Fold, Setting, User, SwitchButton } from '@element-plus/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userRole = computed(() => localStorage.getItem('userRole'))
|
||||
|
@ -87,6 +92,17 @@ const handleLogout = () => {
|
|||
localStorage.removeItem('userRole')
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
const isCollapse = ref(localStorage.getItem('sidebarCollapsed') === 'true')
|
||||
|
||||
const toggleSidebar = () => {
|
||||
isCollapse.value = !isCollapse.value
|
||||
localStorage.setItem('sidebarCollapsed', isCollapse.value)
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
localStorage.setItem('sidebarCollapsed', isCollapse.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -102,22 +118,27 @@ const handleLogout = () => {
|
|||
|
||||
.logo {
|
||||
height: 60px;
|
||||
padding: 10px 20px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #002140;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 12px;
|
||||
margin-right: v-bind('isCollapse ? "0" : "12px"');
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
@ -139,6 +160,7 @@ const handleLogout = () => {
|
|||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
|
@ -193,4 +215,13 @@ const handleLogout = () => {
|
|||
:deep(.el-dropdown-menu__item .el-icon) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 添加菜单折叠样式 */
|
||||
:deep(.el-menu--collapse) {
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
:deep(.el-menu--collapse .el-menu-item) {
|
||||
padding: 0 20px !important;
|
||||
}
|
||||
</style>
|
46
src/layout/VisitorLayout.vue
Normal file
46
src/layout/VisitorLayout.vue
Normal file
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<div class="visitor-layout">
|
||||
<el-container v-loading.fullscreen.lock="loading">
|
||||
<el-main>
|
||||
<router-view></router-view>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, provide, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const loading = ref(false)
|
||||
provide('visitorLoading', loading)
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 监听路由变化来更新标题
|
||||
watch(
|
||||
() => route.meta.title,
|
||||
(newTitle) => {
|
||||
if (newTitle) {
|
||||
document.title = newTitle
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.visitor-layout {
|
||||
min-height: 100vh;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
</style>
|
|
@ -57,9 +57,42 @@ const routes = [
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/visitor',
|
||||
component: () => import('../layout/VisitorLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'VisitorDownload',
|
||||
component: () => import('../views/visitor/DownloadPage.vue'),
|
||||
meta: {
|
||||
public: true,
|
||||
title: 'iFileProxy - 文件代理下载'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tasks',
|
||||
name: 'VisitorTasks',
|
||||
component: () => import('../views/visitor/TaskList.vue'),
|
||||
meta: {
|
||||
public: true,
|
||||
title: 'iFileProxy - 任务列表'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'delete',
|
||||
name: 'VisitorDelete',
|
||||
component: () => import('../views/visitor/DeleteInfo.vue'),
|
||||
meta: {
|
||||
public: true,
|
||||
title: 'iFileProxy - 删除访客信息'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/dashboard'
|
||||
redirect: '/visitor'
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -70,6 +103,12 @@ const router = createRouter({
|
|||
|
||||
// 简化路由守卫
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
// 如果是访客页面,直接放行
|
||||
if (to.path.startsWith('/visitor')) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('token')
|
||||
const isAuthenticated = localStorage.getItem('isAuthenticated')
|
||||
const userRole = localStorage.getItem('userRole')
|
||||
|
@ -106,6 +145,12 @@ router.beforeEach(async (to, from, next) => {
|
|||
}
|
||||
}
|
||||
|
||||
// 如果是公开页面,直接放行
|
||||
if (to.meta.public) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (to.meta.roles && !to.meta.roles.includes(userRole)) {
|
||||
ElMessage.error('没有访问权限')
|
||||
|
@ -116,4 +161,12 @@ router.beforeEach(async (to, from, next) => {
|
|||
next()
|
||||
})
|
||||
|
||||
// 添加全局路由守卫来更新标题
|
||||
router.afterEach((to) => {
|
||||
// 设置默认标题
|
||||
const defaultTitle = '后台管理系统'
|
||||
// 如果路由有title元信息,则使用它,否则使用默认标题
|
||||
document.title = to.meta.title || defaultTitle
|
||||
})
|
||||
|
||||
export default router
|
13
src/utils/visitorTips.js
Normal file
13
src/utils/visitorTips.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
export const tips = [
|
||||
'任务列表每个IP相互隔离,不必担心任务信息泄露。',
|
||||
'哈希算法为MD5。',
|
||||
'下载的文件不会永久存储在服务器上, 每隔一段时间服务器会自动清理那些文件, 并且任务状态会转变为"已被清理"。',
|
||||
'本站会收集一些访客信息用于访客数据分析, 若你不想被收集相关信息用于分析, 请点击"删除我的访客信息"自助删除已经存储在服务器上的信息(新加任务仍然会收集)。',
|
||||
'服务器同时下载的任务达到限制时候, 新的任务会被加入队列(即 排队), 若队列达到管理员设置的最大值则不再接受新任务, 直至队列空出余量。',
|
||||
'本站为了节省成本和照顾其他访客的感受, 每个IP有一定的下载和任务提交配额, 达到限制后当天内不能再下载文件或者新增任务, 请勿浪费公共资源。'
|
||||
]
|
||||
|
||||
export function getRandomTip() {
|
||||
const index = Math.floor(Math.random() * tips.length)
|
||||
return tips[index]
|
||||
}
|
|
@ -244,4 +244,66 @@ onMounted(() => {
|
|||
:deep(.el-tag) {
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
/* 添加响应式样式 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.config-view {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 调整描述列表在移动端的显示 */
|
||||
:deep(.el-descriptions) {
|
||||
.el-descriptions__label {
|
||||
width: 100px;
|
||||
min-width: 100px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.el-descriptions__content {
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.el-descriptions__cell {
|
||||
padding: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 调整标签在移动端的显示 */
|
||||
:deep(.el-tag) {
|
||||
margin: 2px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 调整选项卡在移动端的显示 */
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 13px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
/* 调整刷新按钮 */
|
||||
.card-header .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板端样式调整 */
|
||||
@media screen and (min-width: 769px) and (max-width: 1024px) {
|
||||
.config-view {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="dashboard">
|
||||
<el-row :gutter="20" v-loading="loading" element-loading-text="加载中...">
|
||||
<el-col :span="6">
|
||||
<el-row :gutter="20" v-loading="loading" element-loading-text="加载中..." class="stat-row">
|
||||
<el-col :xs="24" :sm="12" :md="6" class="mb-4">
|
||||
<el-card shadow="hover" class="stat-card total-card">
|
||||
<div class="stat-header">
|
||||
<el-icon class="header-icon"><DataLine /></el-icon>
|
||||
|
@ -14,7 +14,7 @@
|
|||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-col :xs="24" :sm="12" :md="6" class="mb-4">
|
||||
<el-card shadow="hover" class="stat-card running-card">
|
||||
<div class="stat-header">
|
||||
<el-icon class="header-icon"><Loading /></el-icon>
|
||||
|
@ -31,7 +31,7 @@
|
|||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-col :xs="24" :sm="12" :md="6" class="mb-4">
|
||||
<el-card shadow="hover" class="stat-card error-card">
|
||||
<div class="stat-header">
|
||||
<el-icon class="header-icon"><Warning /></el-icon>
|
||||
|
@ -66,7 +66,7 @@
|
|||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-col :xs="24" :sm="12" :md="6" class="mb-4">
|
||||
<el-card shadow="hover" class="stat-card success-card">
|
||||
<div class="stat-header">
|
||||
<el-icon class="header-icon"><Select /></el-icon>
|
||||
|
@ -92,7 +92,7 @@
|
|||
|
||||
<div class="refresh-info">
|
||||
<el-icon><Timer /></el-icon>
|
||||
<span>数据将在 {{ countdown }} 秒后刷新</span>
|
||||
<span class="refresh-text">数据将在 {{ countdown }} 秒后刷新</span>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
|
@ -207,10 +207,12 @@ const hasUnknownStates = computed(() => {
|
|||
}
|
||||
|
||||
.stat-card {
|
||||
height: 160px;
|
||||
height: auto; /* 修改为自适应高度 */
|
||||
min-height: 160px;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 20px; /* 添加内边距 */
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
|
@ -235,10 +237,11 @@ const hasUnknownStates = computed(() => {
|
|||
|
||||
.stat-content {
|
||||
text-align: center;
|
||||
margin: 20px 0; /* 添加上下边距 */
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 36px;
|
||||
font-size: clamp(24px, 5vw, 36px); /* 响应式字体大小 */
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
@ -250,15 +253,17 @@ const hasUnknownStates = computed(() => {
|
|||
}
|
||||
|
||||
.sub-info {
|
||||
position: absolute;
|
||||
position: absolute; /* 修改定位方式 */
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* 允许换行 */
|
||||
align-items: center;
|
||||
gap: 12px; /* 增加间距 */
|
||||
gap: 12px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
margin-top: 20px; /* 添加上边距 */
|
||||
}
|
||||
|
||||
.sub-item {
|
||||
|
@ -284,6 +289,12 @@ const hasUnknownStates = computed(() => {
|
|||
gap: 8px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
flex-wrap: wrap; /* 允许换行 */
|
||||
padding: 0 10px; /* 添加两侧内边距 */
|
||||
}
|
||||
|
||||
.refresh-text {
|
||||
white-space: nowrap; /* 防止文字换行 */
|
||||
}
|
||||
|
||||
/* 卡片主题色 */
|
||||
|
@ -300,6 +311,62 @@ const hasUnknownStates = computed(() => {
|
|||
/* 添加遮罩层样式 */
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* 添加响应式样式 */
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* 移动端样式调整 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.dashboard {
|
||||
padding: 10px; /* 减小内边距 */
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sub-info {
|
||||
font-size: 12px;
|
||||
justify-content: center; /* 居中显示 */
|
||||
}
|
||||
|
||||
.refresh-info {
|
||||
flex-direction: column; /* 垂直排列 */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 15px; /* 减小卡片内边距 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板样式调整 */
|
||||
@media screen and (min-width: 769px) and (max-width: 1024px) {
|
||||
.stat-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
position: relative;
|
||||
min-height: 160px;
|
||||
}
|
||||
</style>
|
|
@ -33,7 +33,7 @@
|
|||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#f5f7fa' }"
|
||||
border>
|
||||
<el-table-column prop="tid" label="任务UUID" width="250" />
|
||||
<el-table-column prop="tid" label="任务UUID" width="350" />
|
||||
<el-table-column prop="client_ip" label="客户端IP" width="140" />
|
||||
<el-table-column prop="add_time" label="添加时间" width="180">
|
||||
<template #default="scope">
|
||||
|
@ -54,7 +54,11 @@
|
|||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="420">
|
||||
<el-table-column
|
||||
label="操作"
|
||||
:width="isMobile ? '70' : '420'"
|
||||
fixed="right"
|
||||
class-name="operation-column">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
|
@ -238,7 +242,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { TaskAPI } from '../api/task'
|
||||
import {
|
||||
|
@ -621,6 +625,24 @@ const getTagType = (tag) => {
|
|||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
// 添加移动端检测
|
||||
const isMobile = computed(() => {
|
||||
return window.innerWidth <= 768
|
||||
})
|
||||
|
||||
// 监听窗口大小变化
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', () => {
|
||||
isMobile.value = window.innerWidth <= 768
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', () => {
|
||||
isMobile.value = window.innerWidth <= 768
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -777,4 +799,175 @@ const getTagType = (tag) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 添加响应式样式 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.task-management {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 让按钮占满宽度 */
|
||||
.header-right .el-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 调整表格在移动端的显示 */
|
||||
:deep(.el-table) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 隐藏不重要的列 */
|
||||
:deep(.el-table .cell) {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* 调整操作按钮布局 */
|
||||
:deep(.el-table .el-button) {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 调整对话框宽度 */
|
||||
:deep(.el-dialog) {
|
||||
width: 90% !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 调整分页器样式 */
|
||||
.pagination-container {
|
||||
.el-pagination {
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px;
|
||||
|
||||
.el-pagination__total,
|
||||
.el-pagination__sizes,
|
||||
.el-pagination__jump {
|
||||
display: none; /* 在移动端隐藏这些元素 */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板端样式调整 */
|
||||
@media screen and (min-width: 769px) and (max-width: 1024px) {
|
||||
.task-management {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button) {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 添加一些通用的响应式辅助类 */
|
||||
.text-ellipsis {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 优化表格在不同屏幕尺寸下的显示 */
|
||||
:deep(.el-table) {
|
||||
@media screen and (max-width: 768px) {
|
||||
/* UUID列宽度调整 */
|
||||
.el-table-column--tid {
|
||||
min-width: 120px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
/* 状态列固定宽度 */
|
||||
.el-table-column--status {
|
||||
width: 15px !important;
|
||||
}
|
||||
|
||||
/* 操作列自适应 */
|
||||
.el-table-column--operation {
|
||||
width: auto !important;
|
||||
min-width: 500px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 优化对话框内容在移动端的显示 */
|
||||
:deep(.el-dialog__body) {
|
||||
@media screen and (max-width: 768px) {
|
||||
padding: 15px;
|
||||
|
||||
.el-descriptions__label {
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.el-descriptions__content {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 优化按钮组在移动端的显示 */
|
||||
.details-actions {
|
||||
@media screen and (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.el-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 优化固定列的样式 */
|
||||
:deep(.operation-column) {
|
||||
background-color: #fff; /* 确保背景色与表格一致 */
|
||||
|
||||
.cell {
|
||||
white-space: nowrap; /* 防止按钮换行 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media screen and (max-width: 768px) {
|
||||
:deep(.operation-column) {
|
||||
.cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 4px !important; /* 减小内边距 */
|
||||
|
||||
.el-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
padding: 4px 0; /* 调整按钮内边距 */
|
||||
margin: 0; /* 移除按钮外边距 */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -28,8 +28,7 @@
|
|||
v-model="searchQuery"
|
||||
placeholder="搜索事件..."
|
||||
class="search-input"
|
||||
clearable
|
||||
@clear="handleSearch">
|
||||
clearable>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
|
@ -44,22 +43,41 @@
|
|||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#f5f7fa' }"
|
||||
border>
|
||||
<el-table-column prop="eventTime" label="时间" width="180">
|
||||
<el-table-column
|
||||
prop="eventTime"
|
||||
label="时间"
|
||||
:width="isMobile ? '100' : '180'">
|
||||
<template #default="scope">
|
||||
{{ formatDateTime(scope.row.eventTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="eventType" label="类型" width="120">
|
||||
<el-table-column
|
||||
prop="eventType"
|
||||
label="类型"
|
||||
:width="isMobile ? '80' : '120'">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getEventTypeStyle(scope.row.eventType)">
|
||||
{{ getEventTypeLabel(scope.row.eventType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="username" label="用户名" width="150" />
|
||||
<el-table-column prop="nickname" label="昵称" width="150" />
|
||||
<el-table-column prop="eventIP" label="IP地址" width="140" />
|
||||
<el-table-column prop="eventDetail" label="详细信息" min-width="200" />
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="用户名"
|
||||
:width="isMobile ? '100' : '150'" />
|
||||
<el-table-column
|
||||
prop="nickname"
|
||||
label="昵称"
|
||||
:width="isMobile ? '100' : '150'" />
|
||||
<el-table-column
|
||||
prop="eventIP"
|
||||
label="IP地址"
|
||||
:width="isMobile ? '100' : '140'" />
|
||||
<el-table-column
|
||||
prop="eventDetail"
|
||||
label="详细信息"
|
||||
min-width="200"
|
||||
show-overflow-tooltip />
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-container">
|
||||
|
@ -78,7 +96,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { ref, computed, onMounted, watch, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { UserAPI } from '../api/user'
|
||||
|
@ -115,10 +133,15 @@ const fetchEvents = async () => {
|
|||
try {
|
||||
const params = {
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: searchQuery.value || undefined
|
||||
pageSize: pageSize.value
|
||||
}
|
||||
|
||||
// 只有当搜索关键词不为空时才添加到参数中
|
||||
if (searchQuery.value.trim()) {
|
||||
params.keyword = searchQuery.value.trim()
|
||||
}
|
||||
|
||||
// 只有超级管理员且选择了用户时才添加用户ID
|
||||
if (isSuperAdmin.value && selectedUserId.value) {
|
||||
params.userId = selectedUserId.value
|
||||
}
|
||||
|
@ -152,19 +175,35 @@ const fetchUserList = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1
|
||||
fetchEvents()
|
||||
// 添加防抖函数
|
||||
const debounce = (fn, delay) => {
|
||||
let timer = null
|
||||
return function (...args) {
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
fn.apply(this, args)
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
// 修改搜索处理
|
||||
const handleSearch = debounce(() => {
|
||||
currentPage.value = 1 // 重置页码
|
||||
fetchEvents()
|
||||
}, 1500) // 1.5秒延迟
|
||||
|
||||
// 监听搜索输入
|
||||
watch(searchQuery, () => {
|
||||
handleSearch()
|
||||
})
|
||||
|
||||
// 处理页码变化
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page
|
||||
fetchEvents()
|
||||
}
|
||||
|
||||
// 处理<EFBFBD><EFBFBD><EFBFBD>页数量变化
|
||||
// 处理页数量变化
|
||||
const handleSizeChange = (size) => {
|
||||
pageSize.value = size
|
||||
currentPage.value = 1
|
||||
|
@ -183,12 +222,26 @@ watch(selectedUserId, () => {
|
|||
fetchEvents()
|
||||
})
|
||||
|
||||
// 初始化
|
||||
// 添加移动端检测
|
||||
const isMobile = computed(() => {
|
||||
return window.innerWidth <= 768
|
||||
})
|
||||
|
||||
// 监听窗口大小变化
|
||||
onMounted(() => {
|
||||
fetchEvents()
|
||||
if (isSuperAdmin.value) {
|
||||
fetchUserList()
|
||||
}
|
||||
window.addEventListener('resize', () => {
|
||||
isMobile.value = window.innerWidth <= 768
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', () => {
|
||||
isMobile.value = window.innerWidth <= 768
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -246,4 +299,120 @@ onMounted(() => {
|
|||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 添加响应式样式 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.events-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-input,
|
||||
.user-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 调整表格在移动端的显示 */
|
||||
:deep(.el-table) {
|
||||
font-size: 12px;
|
||||
|
||||
.cell {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.el-tag {
|
||||
font-size: 11px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 调整分页器样式 */
|
||||
.pagination-container {
|
||||
.el-pagination {
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px;
|
||||
|
||||
.el-pagination__total,
|
||||
.el-pagination__sizes,
|
||||
.el-pagination__jump {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板端样式调整 */
|
||||
@media screen and (min-width: 769px) and (max-width: 1024px) {
|
||||
.events-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.user-select {
|
||||
width: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 优化表格在不同屏幕尺寸下的显示 */
|
||||
:deep(.el-table) {
|
||||
@media screen and (max-width: 768px) {
|
||||
/* 时间列宽度调整 */
|
||||
.el-table-column--eventTime {
|
||||
min-width: 100px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
/* 类型列固定宽度 */
|
||||
.el-table-column--eventType {
|
||||
width: 80px !important;
|
||||
}
|
||||
|
||||
/* 用户名和昵称列宽度 */
|
||||
.el-table-column--username,
|
||||
.el-table-column--nickname {
|
||||
min-width: 80px;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
/* IP地址列宽度 */
|
||||
.el-table-column--eventIP {
|
||||
min-width: 90px;
|
||||
max-width: 110px;
|
||||
}
|
||||
|
||||
/* 详细信息列自适应 */
|
||||
.el-table-column--eventDetail {
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 优化标签显示 */
|
||||
:deep(.el-tag) {
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 优化详细信息显示 */
|
||||
:deep(.el-table__row) {
|
||||
.cell {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -50,7 +50,11 @@
|
|||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right">
|
||||
<el-table-column
|
||||
label="操作"
|
||||
:width="isMobile ? '80' : '300'"
|
||||
fixed="right"
|
||||
class-name="operation-column">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
|
@ -177,7 +181,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Edit, EditPen, Key } from '@element-plus/icons-vue'
|
||||
import { UserAPI } from '../api/user'
|
||||
|
@ -394,7 +398,7 @@ const submitResetPassword = async () => {
|
|||
resetPasswordForm.value.newPassword
|
||||
)
|
||||
if (response.retcode === 0) {
|
||||
ElMessage.success('密码重置成功')
|
||||
ElMessage.success('<EFBFBD><EFBFBD><EFBFBD>码重置成功')
|
||||
resetPasswordDialogVisible.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -454,9 +458,23 @@ const submitUpdateNickname = async () => {
|
|||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
// 添加移动端检测
|
||||
const isMobile = computed(() => {
|
||||
return window.innerWidth <= 768
|
||||
})
|
||||
|
||||
// 监听窗口大小变化
|
||||
onMounted(() => {
|
||||
fetchUsers()
|
||||
window.addEventListener('resize', () => {
|
||||
isMobile.value = window.innerWidth <= 768
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', () => {
|
||||
isMobile.value = window.innerWidth <= 768
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -520,4 +538,93 @@ onMounted(() => {
|
|||
:deep(.el-form-item__label) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 添加响应式样式 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.user-management {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 调整表格在移动端的显示 */
|
||||
:deep(.el-table) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-table .cell) {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* 调整操作按钮布局 */
|
||||
:deep(.operation-column) {
|
||||
.cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 4px !important;
|
||||
|
||||
.el-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
padding: 4px 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 调整对话框宽度 */
|
||||
:deep(.el-dialog) {
|
||||
width: 90% !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 调整分页器样式 */
|
||||
.pagination-container {
|
||||
.el-pagination {
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px;
|
||||
|
||||
.el-pagination__total,
|
||||
.el-pagination__sizes,
|
||||
.el-pagination__jump {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 优化表格在不同屏幕尺寸下的显示 */
|
||||
:deep(.el-table) {
|
||||
@media screen and (max-width: 768px) {
|
||||
/* 用户名列宽度调整 */
|
||||
.el-table-column--username {
|
||||
min-width: 100px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
/* 权限列固定宽度 */
|
||||
.el-table-column--mask {
|
||||
width: 80px !important;
|
||||
}
|
||||
|
||||
/* 操作列自适应 */
|
||||
.el-table-column--operation {
|
||||
width: auto !important;
|
||||
min-width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -7,7 +7,10 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions
|
||||
:column="isMobile ? 1 : 2"
|
||||
border
|
||||
class="profile-descriptions">
|
||||
<el-descriptions-item label="用户ID">
|
||||
{{ userInfo.userId }}
|
||||
</el-descriptions-item>
|
||||
|
@ -39,10 +42,10 @@
|
|||
</el-descriptions>
|
||||
|
||||
<div class="profile-actions">
|
||||
<el-button type="primary" @click="showNicknameDialog">
|
||||
<el-button type="primary" @click="showNicknameDialog" class="action-button">
|
||||
<el-icon><Edit /></el-icon>修改昵称
|
||||
</el-button>
|
||||
<el-button type="warning" @click="showPasswordDialog">
|
||||
<el-button type="warning" @click="showPasswordDialog" class="action-button">
|
||||
<el-icon><Lock /></el-icon>修改密码
|
||||
</el-button>
|
||||
</div>
|
||||
|
@ -119,7 +122,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Edit, Lock } from '@element-plus/icons-vue'
|
||||
import { UserAPI } from '../api/user'
|
||||
|
@ -289,6 +292,24 @@ const handleUpdatePassword = async () => {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 添加移动端检测
|
||||
const isMobile = computed(() => {
|
||||
return window.innerWidth <= 768
|
||||
})
|
||||
|
||||
// 监听窗口大小变化
|
||||
const resizeHandler = () => {
|
||||
isMobile.value = window.innerWidth <= 768
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', resizeHandler)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', resizeHandler)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -329,7 +350,48 @@ const handleUpdatePassword = async () => {
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-weight: 500;
|
||||
.action-button {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* 添加响应式样式 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.profile-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 调整对话框样式 */
|
||||
:deep(.el-dialog) {
|
||||
width: 90% !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.profile-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__cell) {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 90px;
|
||||
min-width: 90px;
|
||||
}
|
||||
}
|
||||
</style>
|
155
src/views/visitor/DeleteInfo.vue
Normal file
155
src/views/visitor/DeleteInfo.vue
Normal file
|
@ -0,0 +1,155 @@
|
|||
<template>
|
||||
<div class="delete-info">
|
||||
<el-card class="delete-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>删除访客信息</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="data-count">
|
||||
{{ dataCountText }}
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
type="danger"
|
||||
:disabled="!canDelete"
|
||||
:loading="isDeleting"
|
||||
@click="handleDelete"
|
||||
class="delete-button">
|
||||
删除我的访客信息
|
||||
</el-button>
|
||||
|
||||
<div class="action-links">
|
||||
<el-link type="primary" @click="$router.push('/visitor')">
|
||||
返回主页
|
||||
</el-link>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, inject } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { VisitorAPI } from '@/api/visitor'
|
||||
|
||||
const loading = inject('visitorLoading')
|
||||
const isDeleting = ref(false)
|
||||
const tasks = ref([])
|
||||
|
||||
const dataCountText = computed(() => {
|
||||
if (loading.value) return '正在加载数据...'
|
||||
|
||||
if (!Array.isArray(tasks.value)) {
|
||||
return '正在加载数据...'
|
||||
}
|
||||
|
||||
const count = tasks.value.length
|
||||
if (count === 0) return '服务器上没有存储任何关于你的数据'
|
||||
if (tasks.value.some(task => task.status === 1)) {
|
||||
return '现在有正在运行中的任务, 你现在无法删除你的访客信息'
|
||||
}
|
||||
return `服务器上有 ${count} 条数据`
|
||||
})
|
||||
|
||||
const canDelete = computed(() => {
|
||||
if (!Array.isArray(tasks.value)) {
|
||||
return false
|
||||
}
|
||||
return tasks.value.length > 0 && !tasks.value.some(task => task.status === 1)
|
||||
})
|
||||
|
||||
const fetchTasks = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await VisitorAPI.getMyTasks()
|
||||
if (response.retcode === 0) {
|
||||
tasks.value = Array.isArray(response.data) ? response.data : []
|
||||
} else {
|
||||
ElMessage.error(response.message)
|
||||
tasks.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取数据失败')
|
||||
tasks.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'如果你删除你的信息,任务管理中的所有数据都会被删除,无法找回。你确定要继续吗?',
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
isDeleting.value = true
|
||||
const response = await VisitorAPI.deleteVisitorInfo()
|
||||
if (response.retcode === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
await fetchTasks()
|
||||
} else {
|
||||
ElMessage.error(response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败,请重试')
|
||||
}
|
||||
} finally {
|
||||
isDeleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchTasks()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.delete-info {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.delete-card {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.data-count {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-links {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.delete-info {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
166
src/views/visitor/DownloadPage.vue
Normal file
166
src/views/visitor/DownloadPage.vue
Normal file
|
@ -0,0 +1,166 @@
|
|||
<template>
|
||||
<div class="download-page">
|
||||
<el-card class="download-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>iFileProxy 离线下载</h2>
|
||||
<div class="subtitle">
|
||||
本程序可实现文件代理下载,如加速Github和一些墙内无法访问的文件等,程序开源免费
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form @submit.prevent="handleSubmit">
|
||||
<el-form-item label="目标文件URL">
|
||||
<el-input
|
||||
v-model="downloadUrl"
|
||||
placeholder="请输入要下载的文件链接"
|
||||
:disabled="loading" />
|
||||
</el-form-item>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="loading"
|
||||
class="submit-btn">
|
||||
提交
|
||||
</el-button>
|
||||
</el-form>
|
||||
|
||||
<div class="status-info">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="运行中任务">
|
||||
{{ serverLoad.running }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="排队中任务">
|
||||
{{ serverLoad.queuing }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="action-links">
|
||||
<el-link type="primary" @click="$router.push('/visitor/tasks')">
|
||||
查询文件下载任务状态
|
||||
</el-link>
|
||||
<el-divider direction="vertical" />
|
||||
<el-link type="danger" @click="$router.push('/visitor/delete')">
|
||||
删除我的访客信息
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<div class="tips">
|
||||
<el-alert
|
||||
:title="currentTip"
|
||||
type="info"
|
||||
:closable="false" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, inject } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getRandomTip } from '@/utils/visitorTips'
|
||||
import { VisitorAPI } from '@/api/visitor'
|
||||
|
||||
const loading = inject('visitorLoading')
|
||||
const downloadUrl = ref('')
|
||||
const serverLoad = ref({
|
||||
running: 0,
|
||||
queuing: 0
|
||||
})
|
||||
|
||||
const currentTip = ref(getRandomTip())
|
||||
|
||||
// 获取服务器负载信息
|
||||
const fetchServerLoad = async () => {
|
||||
try {
|
||||
const response = await VisitorAPI.getServerLoad()
|
||||
if (response.retcode === 0) {
|
||||
serverLoad.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取服务器负载失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交下载任务
|
||||
const handleSubmit = async () => {
|
||||
if (!downloadUrl.value) {
|
||||
ElMessage.warning('请输入下载链接')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await VisitorAPI.addOfflineTask(downloadUrl.value)
|
||||
if (response.retcode === 0) {
|
||||
ElMessage.success('任务提交成功!请点击下方链接查看任务状态')
|
||||
downloadUrl.value = ''
|
||||
} else {
|
||||
ElMessage.error(response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('提交任务失败,请重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchServerLoad()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.download-page {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
min-height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.action-links {
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.download-page {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 12px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
210
src/views/visitor/TaskList.vue
Normal file
210
src/views/visitor/TaskList.vue
Normal file
|
@ -0,0 +1,210 @@
|
|||
<template>
|
||||
<div class="task-list">
|
||||
<el-card class="task-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>离线下载任务管理</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tasks"
|
||||
style="width: 100%"
|
||||
border>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="200">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.status === 3 || row.status === 4">
|
||||
<el-link
|
||||
type="primary"
|
||||
:href="`${API_BASE_URL}/Download/${row.tid}`">
|
||||
{{ row.file_name }}
|
||||
</el-link>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ row.file_name }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
label="大小"
|
||||
prop="size"
|
||||
:formatter="formatFileSize"
|
||||
width="120"
|
||||
class-name="hidden-on-mobile" />
|
||||
|
||||
<el-table-column
|
||||
label="提交时间"
|
||||
width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.add_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
label="结束时间"
|
||||
width="180"
|
||||
class-name="hidden-on-mobile">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.update_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
label="状态"
|
||||
width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
{{ getStatusLabel(row.status, row.queue_position) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
label="哈希"
|
||||
prop="hash"
|
||||
min-width="200"
|
||||
class-name="hidden-on-mobile">
|
||||
<template #default="{ row }">
|
||||
{{ row.hash || 'N/A' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="action-links">
|
||||
<el-link type="primary" @click="$router.push('/visitor')">
|
||||
返回主页
|
||||
</el-link>
|
||||
<el-divider direction="vertical" />
|
||||
<el-link type="danger" @click="$router.push('/visitor/delete')">
|
||||
删除我的访客信息
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<div class="tips">
|
||||
<el-alert
|
||||
:title="currentTip"
|
||||
type="info"
|
||||
:closable="false" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, inject } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getRandomTip } from '@/utils/visitorTips'
|
||||
import { VisitorAPI } from '@/api/visitor'
|
||||
import { API_BASE_URL } from '@/config/api.config'
|
||||
|
||||
const loading = inject('visitorLoading')
|
||||
const tasks = ref([])
|
||||
|
||||
// 状态映射
|
||||
const statusMap = {
|
||||
0: { label: '未初始化', type: 'info' },
|
||||
1: { label: '进行中', type: 'primary' },
|
||||
2: { label: '错误', type: 'danger' },
|
||||
3: { label: '已完成', type: 'success' },
|
||||
4: { label: '已缓存', type: 'success' },
|
||||
5: { label: '已被清理', type: 'info' },
|
||||
6: { label: '排队中', type: 'warning' }
|
||||
}
|
||||
|
||||
const getStatusType = (status) => {
|
||||
return statusMap[status]?.type || 'info'
|
||||
}
|
||||
|
||||
const getStatusLabel = (status, queuePosition) => {
|
||||
const label = statusMap[status]?.label || '未知状态'
|
||||
return status === 6 && queuePosition > 0 ? `${label} #${queuePosition}` : label
|
||||
}
|
||||
|
||||
const formatDateTime = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
return new Date(dateStr).toLocaleString()
|
||||
}
|
||||
|
||||
const formatFileSize = (row, column, cellValue) => {
|
||||
if (!cellValue) return '0 B'
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let size = cellValue
|
||||
let unitIndex = 0
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
const fetchTasks = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await VisitorAPI.getMyTasks()
|
||||
if (response.retcode === 0) {
|
||||
tasks.value = Array.isArray(response.data) ? response.data : []
|
||||
} else {
|
||||
ElMessage.error(response.message)
|
||||
tasks.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取任务列表失败')
|
||||
tasks.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const currentTip = ref(getRandomTip())
|
||||
|
||||
onMounted(() => {
|
||||
fetchTasks()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-list {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
min-height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-links {
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.task-list {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
:deep(.hidden-on-mobile) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-table .cell) {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
Reference in a new issue