把原来bootstrap写的那几个页面页用Vue重写了一遍 对手机端进行页面优化

This commit is contained in:
root 2024-12-01 12:52:41 +08:00
parent e402340dc9
commit d66c6945d1
16 changed files with 1447 additions and 65 deletions

View file

@ -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" />

View file

@ -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
View 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'
}
})
}
}

View file

@ -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>

View 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>

View file

@ -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
View 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]
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View 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>