增加移动端,可以查看统计数据和日志

This commit is contained in:
jianzhichu
2025-12-11 22:16:03 +08:00
parent fc7886a76e
commit d0c4322907
16 changed files with 1531 additions and 164 deletions
+57 -3
View File
@@ -12,12 +12,12 @@ namespace dy.net.Controllers
public class LogsController : ControllerBase public class LogsController : ControllerBase
{ {
private readonly IWebHostEnvironment webHostEnvironment; private readonly IWebHostEnvironment webHostEnvironment;
private readonly DouyinHttpClientService douyinHttpClientService; private readonly LogInfoService logInfoService;
public LogsController(IWebHostEnvironment webHostEnvironment,DouyinHttpClientService douyinHttpClientService) public LogsController(IWebHostEnvironment webHostEnvironment,LogInfoService logInfoService)
{ {
this.webHostEnvironment = webHostEnvironment; this.webHostEnvironment = webHostEnvironment;
this.douyinHttpClientService = douyinHttpClientService; this.logInfoService = logInfoService;
} }
[HttpGet] [HttpGet]
@@ -38,5 +38,59 @@ namespace dy.net.Controllers
//var fileContent = encoding.GetString(fileBytes); //var fileContent = encoding.GetString(fileBytes);
//return Content (fileContent, "text/plain", encoding); //return Content (fileContent, "text/plain", encoding);
} }
/// <summary>
/// 获取最近10天的日志文件列表
/// </summary>
[HttpGet("/api/logs/list")]
public IActionResult GetLogFiles()
{
try
{
var _logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs");
var files = logInfoService.GetLogFiles(_logDirectory);
return Ok(new {code=0,data=files});
}
catch (Exception ex)
{
return StatusCode(500, new { message = "获取日志列表失败", error = ex.Message });
}
}
/// <summary>
/// 获取日志文件内容流
/// </summary>
[HttpGet("/api/logs/content")]
public IActionResult GetLogContent([FromQuery] LogContentRequest request)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
try
{
var _logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs");
var stream = logInfoService.GetLogFileStream(_logDirectory, request.Type, request.Date);
// 返回文件流,指定MIME类型为文本
return File(stream, "text/plain", $"log-{request.Type}-{request.Date}.txt");
}
catch (FileNotFoundException ex)
{
return NotFound(new { message = ex.Message });
}
catch (ArgumentException ex)
{
return BadRequest(new { message = ex.Message });
}
catch (Exception ex)
{
return StatusCode(500, new { message = "读取日志文件失败", error = ex.Message });
}
}
} }
} }
@@ -5,7 +5,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\</_PublishTargetUrl> <_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\</_PublishTargetUrl>
<History>True|2025-12-08T13:20:37.8369271Z||;False|2025-12-08T21:20:21.1646252+08:00||;True|2025-12-08T20:51:50.6845619+08:00||;True|2025-12-08T16:43:45.2425324+08:00||;False|2025-12-08T16:42:23.0308163+08:00||;True|2025-12-08T16:41:52.4103868+08:00||;True|2025-12-08T16:41:49.8972006+08:00||;False|2025-12-08T16:41:39.5343442+08:00||;True|2025-12-08T12:40:19.2430740+08:00||;True|2025-12-08T12:02:30.6005542+08:00||;True|2025-12-08T11:34:29.6673185+08:00||;False|2025-12-08T11:30:56.2857042+08:00||;True|2025-12-08T10:52:43.7427594+08:00||;True|2025-12-08T07:05:03.2845531+08:00||;True|2025-12-07T23:02:10.0037815+08:00||;True|2025-12-07T22:48:25.7859648+08:00||;True|2025-12-07T20:49:13.6683281+08:00||;True|2025-12-07T08:47:30.1236366+08:00||;True|2025-12-06T13:14:32.4410202+08:00||;True|2025-12-06T13:07:29.7182102+08:00||;True|2025-12-06T13:05:46.2855366+08:00||;False|2025-12-06T13:05:40.3550904+08:00||;True|2025-12-06T13:03:47.8773880+08:00||;True|2025-12-06T13:03:28.9521030+08:00||;True|2025-12-05T23:11:51.5026151+08:00||;True|2025-12-05T12:46:26.1415079+08:00||;False|2025-12-05T12:46:19.1274332+08:00||;True|2025-12-05T09:04:36.7111717+08:00||;True|2025-12-05T08:22:51.7343317+08:00||;True|2025-12-05T08:20:32.0383739+08:00||;True|2025-12-04T21:58:29.4112766+08:00||;True|2025-12-04T21:58:10.0382218+08:00||;True|2025-12-04T21:57:55.3894059+08:00||;True|2025-12-04T21:10:38.8943797+08:00||;True|2025-12-04T21:09:31.5024569+08:00||;True|2025-12-04T08:22:47.9617427+08:00||;False|2025-12-04T08:22:41.3759733+08:00||;True|2025-12-04T08:14:02.2478893+08:00||;True|2025-12-04T07:57:00.2208139+08:00||;True|2025-12-03T23:58:13.3693303+08:00||;True|2025-12-03T23:57:40.4257893+08:00||;True|2025-12-03T23:56:38.8717769+08:00||;True|2025-12-03T23:56:24.0663568+08:00||;False|2025-12-03T23:55:36.1902974+08:00||;True|2025-12-03T23:51:30.6688504+08:00||;True|2025-12-03T23:34:17.1648971+08:00||;False|2025-12-03T23:34:07.7649161+08:00||;True|2025-12-03T22:32:56.2435003+08:00||;True|2025-12-03T22:27:45.6059666+08:00||;True|2025-12-03T15:55:28.0186597+08:00||;False|2025-12-03T15:54:17.5032724+08:00||;True|2025-12-03T15:53:10.2273509+08:00||;True|2025-12-02T22:11:37.4645042+08:00||;False|2025-12-02T22:10:47.1320259+08:00||;True|2025-12-02T14:39:58.8932130+08:00||;True|2025-12-02T14:36:37.1529072+08:00||;True|2025-12-02T12:58:22.0951548+08:00||;True|2025-12-02T09:30:33.7066474+08:00||;True|2025-12-02T09:30:14.5481844+08:00||;True|2025-12-02T09:30:00.3123364+08:00||;False|2025-12-02T09:29:54.4615520+08:00||;True|2025-12-02T09:13:01.7995414+08:00||;False|2025-12-02T09:12:55.8360281+08:00||;True|2025-12-02T09:12:31.0156791+08:00||;True|2025-12-02T08:55:11.3773395+08:00||;True|2025-12-02T08:53:21.3927713+08:00||;False|2025-12-02T08:48:55.8638037+08:00||;True|2025-12-02T07:29:25.2192447+08:00||;False|2025-12-02T07:29:10.2504665+08:00||;True|2025-12-02T07:28:29.0769286+08:00||;True|2025-12-01T21:53:07.6474834+08:00||;True|2025-12-01T21:44:28.1674315+08:00||;True|2025-12-01T21:18:47.2119609+08:00||;True|2025-12-01T20:51:19.9948301+08:00||;True|2025-12-01T20:50:36.9904697+08:00||;True|2025-12-01T20:44:10.1695142+08:00||;True|2025-12-01T19:27:58.0479456+08:00||;True|2025-12-01T19:16:02.2288214+08:00||;False|2025-12-01T19:15:56.0372115+08:00||;True|2025-12-01T19:15:16.4854821+08:00||;False|2025-12-01T19:15:06.7001019+08:00||;True|2025-12-01T17:10:38.9739466+08:00||;False|2025-12-01T17:10:18.8928139+08:00||;True|2025-12-01T17:03:59.3171589+08:00||;True|2025-12-01T17:03:12.5681656+08:00||;True|2025-12-01T16:42:51.4415025+08:00||;True|2025-12-01T01:29:59.6975567+08:00||;True|2025-12-01T01:29:45.1898652+08:00||;False|2025-12-01T01:29:33.1787721+08:00||;True|2025-12-01T01:27:33.1297605+08:00||;True|2025-12-01T01:27:27.3976862+08:00||;False|2025-12-01T01:27:20.5193052+08:00||;True|2025-12-01T01:11:11.0210582+08:00||;True|2025-12-01T01:04:04.3307782+08:00||;True|2025-12-01T00:55:49.8018864+08:00||;True|2025-12-01T00:27:25.1323561+08:00||;True|2025-11-30T23:58:20.7284116+08:00||;True|2025-11-30T23:57:58.3027622+08:00||;True|2025-11-30T23:57:43.0829614+08:00||;False|2025-11-30T23:57:33.5606716+08:00||;</History> <History>True|2025-12-11T14:07:16.6229994Z||;True|2025-12-11T22:05:56.7363206+08:00||;True|2025-12-11T22:01:07.5862406+08:00||;True|2025-12-11T22:00:43.5796594+08:00||;True|2025-12-08T21:20:37.8369271+08:00||;False|2025-12-08T21:20:21.1646252+08:00||;True|2025-12-08T20:51:50.6845619+08:00||;True|2025-12-08T16:43:45.2425324+08:00||;False|2025-12-08T16:42:23.0308163+08:00||;True|2025-12-08T16:41:52.4103868+08:00||;True|2025-12-08T16:41:49.8972006+08:00||;False|2025-12-08T16:41:39.5343442+08:00||;True|2025-12-08T12:40:19.2430740+08:00||;True|2025-12-08T12:02:30.6005542+08:00||;True|2025-12-08T11:34:29.6673185+08:00||;False|2025-12-08T11:30:56.2857042+08:00||;True|2025-12-08T10:52:43.7427594+08:00||;True|2025-12-08T07:05:03.2845531+08:00||;True|2025-12-07T23:02:10.0037815+08:00||;True|2025-12-07T22:48:25.7859648+08:00||;True|2025-12-07T20:49:13.6683281+08:00||;True|2025-12-07T08:47:30.1236366+08:00||;True|2025-12-06T13:14:32.4410202+08:00||;True|2025-12-06T13:07:29.7182102+08:00||;True|2025-12-06T13:05:46.2855366+08:00||;False|2025-12-06T13:05:40.3550904+08:00||;True|2025-12-06T13:03:47.8773880+08:00||;True|2025-12-06T13:03:28.9521030+08:00||;True|2025-12-05T23:11:51.5026151+08:00||;True|2025-12-05T12:46:26.1415079+08:00||;False|2025-12-05T12:46:19.1274332+08:00||;True|2025-12-05T09:04:36.7111717+08:00||;True|2025-12-05T08:22:51.7343317+08:00||;True|2025-12-05T08:20:32.0383739+08:00||;True|2025-12-04T21:58:29.4112766+08:00||;True|2025-12-04T21:58:10.0382218+08:00||;True|2025-12-04T21:57:55.3894059+08:00||;True|2025-12-04T21:10:38.8943797+08:00||;True|2025-12-04T21:09:31.5024569+08:00||;True|2025-12-04T08:22:47.9617427+08:00||;False|2025-12-04T08:22:41.3759733+08:00||;True|2025-12-04T08:14:02.2478893+08:00||;True|2025-12-04T07:57:00.2208139+08:00||;True|2025-12-03T23:58:13.3693303+08:00||;True|2025-12-03T23:57:40.4257893+08:00||;True|2025-12-03T23:56:38.8717769+08:00||;True|2025-12-03T23:56:24.0663568+08:00||;False|2025-12-03T23:55:36.1902974+08:00||;True|2025-12-03T23:51:30.6688504+08:00||;True|2025-12-03T23:34:17.1648971+08:00||;False|2025-12-03T23:34:07.7649161+08:00||;True|2025-12-03T22:32:56.2435003+08:00||;True|2025-12-03T22:27:45.6059666+08:00||;True|2025-12-03T15:55:28.0186597+08:00||;False|2025-12-03T15:54:17.5032724+08:00||;True|2025-12-03T15:53:10.2273509+08:00||;True|2025-12-02T22:11:37.4645042+08:00||;False|2025-12-02T22:10:47.1320259+08:00||;True|2025-12-02T14:39:58.8932130+08:00||;True|2025-12-02T14:36:37.1529072+08:00||;True|2025-12-02T12:58:22.0951548+08:00||;True|2025-12-02T09:30:33.7066474+08:00||;True|2025-12-02T09:30:14.5481844+08:00||;True|2025-12-02T09:30:00.3123364+08:00||;False|2025-12-02T09:29:54.4615520+08:00||;True|2025-12-02T09:13:01.7995414+08:00||;False|2025-12-02T09:12:55.8360281+08:00||;True|2025-12-02T09:12:31.0156791+08:00||;True|2025-12-02T08:55:11.3773395+08:00||;True|2025-12-02T08:53:21.3927713+08:00||;False|2025-12-02T08:48:55.8638037+08:00||;True|2025-12-02T07:29:25.2192447+08:00||;False|2025-12-02T07:29:10.2504665+08:00||;True|2025-12-02T07:28:29.0769286+08:00||;True|2025-12-01T21:53:07.6474834+08:00||;True|2025-12-01T21:44:28.1674315+08:00||;True|2025-12-01T21:18:47.2119609+08:00||;True|2025-12-01T20:51:19.9948301+08:00||;True|2025-12-01T20:50:36.9904697+08:00||;True|2025-12-01T20:44:10.1695142+08:00||;True|2025-12-01T19:27:58.0479456+08:00||;True|2025-12-01T19:16:02.2288214+08:00||;False|2025-12-01T19:15:56.0372115+08:00||;True|2025-12-01T19:15:16.4854821+08:00||;False|2025-12-01T19:15:06.7001019+08:00||;True|2025-12-01T17:10:38.9739466+08:00||;False|2025-12-01T17:10:18.8928139+08:00||;True|2025-12-01T17:03:59.3171589+08:00||;True|2025-12-01T17:03:12.5681656+08:00||;True|2025-12-01T16:42:51.4415025+08:00||;True|2025-12-01T01:29:59.6975567+08:00||;True|2025-12-01T01:29:45.1898652+08:00||;False|2025-12-01T01:29:33.1787721+08:00||;True|2025-12-01T01:27:33.1297605+08:00||;True|2025-12-01T01:27:27.3976862+08:00||;False|2025-12-01T01:27:20.5193052+08:00||;True|2025-12-01T01:11:11.0210582+08:00||;True|2025-12-01T01:04:04.3307782+08:00||;True|2025-12-01T00:55:49.8018864+08:00||;True|2025-12-01T00:27:25.1323561+08:00||;</History>
<LastFailureDetails /> <LastFailureDetails />
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
+4 -4
View File
@@ -97,8 +97,8 @@ Cookie 及 `sec_user_id` 是同步功能的核心,需严格按步骤获取,
| 镜像标签 | 架构 | | 镜像标签 | 架构 |
| ----------------- | -------------- | | ----------------- | -------------- |
| `beta_1.8.5` | x86_64 (amd64) | | `beta_1.8.6` | x86_64 (amd64) |
| `arm_1.8.5` | ARM64 | | `arm_1.8.6` | ARM64 |
### 最新镜像查看 ### 最新镜像查看
[镜像列表](http://nas.synology2023.online:10108/api/docker/dysync/1) [镜像列表](http://nas.synology2023.online:10108/api/docker/dysync/1)
@@ -116,7 +116,7 @@ docker run -d --restart=always \
-v /opt/dysync/uper:/app/uper \ -v /opt/dysync/uper:/app/uper \
-p 10103:10101 \ -p 10103:10101 \
--name dysync2025 \ --name dysync2025 \
ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_1.8.5 ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_1.8.6
# 注意:-p 后面的容器端口,可以用环境变量类似:ASPNETCORE_URLS = http://+:10108 指定 # 注意:-p 后面的容器端口,可以用环境变量类似:ASPNETCORE_URLS = http://+:10108 指定
``` ```
@@ -130,7 +130,7 @@ version: '3.8'
services: services:
dysync: dysync:
image: ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_1.8.5 image: ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_1.8.6
container_name: dysync2025 # 容器名称 container_name: dysync2025 # 容器名称
restart: unless-stopped # 始终重启容器,除非容器被手动停止或Docker服务停止 restart: unless-stopped # 始终重启容器,除非容器被手动停止或Docker服务停止
ports: ports:
+3 -1
View File
@@ -4,7 +4,9 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/logo.png" /> <link rel="icon" type="image/png" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- <meta name="viewport" content="width=device-width, initial-scale=1.0" /> -->
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>抖小云</title> <title>抖小云</title>
</head> </head>
+4 -1
View File
@@ -34,7 +34,7 @@ const { logout } = useAccountStore();
const showPersonalDrawer = ref<boolean>(false); const showPersonalDrawer = ref<boolean>(false);
const personalRef = ref(null); const personalRef = ref(null);
const emailRef = ref(null); const emailRef = ref(null);
const open = ref(true); const open = ref(false);
const showSetting = ref(false); const showSetting = ref(false);
const router = useRouter(); const router = useRouter();
@@ -117,6 +117,9 @@ onMounted(() => {
color: #722ed1 !important; color: #722ed1 !important;
margin-left: 0em !important; margin-left: 0em !important;
} }
.ant-tabs-extra-content {
display: none !important;
}
} }
html { html {
+82 -26
View File
@@ -1,45 +1,107 @@
<script lang="ts" setup> <script lang="ts" setup>
import { LogoutOutlined } from '@ant-design/icons-vue'; import { LogoutOutlined } from '@ant-design/icons-vue';
import { onMounted } from 'vue'; import { onMounted, onUnmounted, computed } from 'vue';
import { ThemeProvider, alert } from 'stepin'; import { ThemeProvider, alert } from 'stepin';
import http from '@/store/http'; import http from '@/store/http';
import { useRouter } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
const router = useRouter(); const router = useRouter();
// console.log(router.getRoutes()); const route = useRoute();
// 核心:精准判断是否为手机浏览器(多重校验)
const isMobileBrowser = computed(() => {
if (typeof navigator === 'undefined' || typeof window === 'undefined') {
return false;
}
const userAgent = navigator.userAgent.toLowerCase();
// 1. 匹配手机UA特征(排除平板/桌面端)
const mobileUA = /android|iphone|ipod|blackberry|windows phone|iemobile|opera mini/i.test(userAgent);
const isTablet = /ipad|tablet|playbook|kindle|android 3\.|android 4\.[0-3]/.test(userAgent); // 排除平板
// 2. 触摸设备校验(手机核心特征)
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
// 3. 屏幕尺寸兜底(适配响应式/模拟器)
const isMobileScreen = window.innerWidth <= 768 && window.innerHeight <= 1024;
// 最终判定:UA是手机 + 触摸设备 或 小屏触摸设备(覆盖所有手机场景)
return (mobileUA && !isTablet && isTouchDevice) || (isMobileScreen && isTouchDevice);
});
// 路由守卫:手机端强制拦截所有路由,仅允许/mobile
const enforceMobileRoute = () => {
const targetMobilePath = '/mobile';
const currentPath = route.path.toLowerCase().trim();
// 核心规则:手机浏览器 → 强制跳转到/mobile(无论当前路由是什么)
console.log(isMobileBrowser.value);
if (isMobileBrowser.value) {
if (currentPath !== targetMobilePath) {
// 替换路由(禁止返回上一页,避免用户回退到其他路由)
router.replace({ path: targetMobilePath, replace: true });
}
} else {
// PC端:禁止访问mobile路由,强制跳转到dashboard
if (currentPath === targetMobilePath) {
router.replace({ path: '/dashboard', replace: true });
}
}
};
// 监听路由变化:确保跳转后仍拦截(防止手动输入URL)
const routerGuard = router.afterEach(() => {
if (http.checkAuthorization()) {
// 仅登录后生效
enforceMobileRoute();
}
});
onMounted(() => { onMounted(() => {
// alert.info( // 1. 登录状态校验
// `<div class="text-text">
// Stepin is a fast, light framework to Vue3 try it out today with the
// <span class="underline">Stepin Template Beta</span>.
// </div>`,
// { renderRaw: true, duration: -1 }
// );
// console.log(router.getRoutes());
if (http.checkAuthorization()) { if (http.checkAuthorization()) {
// console.log(22222); // 2. 初始化立即拦截路由
router.push('/dashboard'); enforceMobileRoute();
// 3. 监听窗口大小变化(适配手机横屏/竖屏切换、模拟器调整尺寸)
window.addEventListener('resize', enforceMobileRoute);
} else { } else {
// 未登录先跳登录页,登录后再拦截
router.push('/login'); router.push('/login');
} }
}); });
// 组件卸载:清理监听,防止内存泄漏
onUnmounted(() => {
window.removeEventListener('resize', enforceMobileRoute);
// 移除路由守卫
routerGuard();
});
</script> </script>
<template> <template>
<ThemeProvider :color="{ middle: { 'bg-base': '#fff','bg-container':'#fff','bg-container-light':'#fff' }, primary: { DEFAULT: '#1896ff' } }" :autoAdapt="true"> <ThemeProvider :color="{
middle: { 'bg-base': '#fff','bg-container':'#fff','bg-container-light':'#fff' },
primary: { DEFAULT: '#1896ff' }
}" :autoAdapt="true">
<div class="front-view flex flex-col" style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)"> <div class="front-view flex flex-col" style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)">
<!-- <div class="text-xxl text-text hover:text-text" style="margin-left:20px;color:#722ed1;"> --> <div class="front-content">
<!-- <img src="/logo.png" /> -->
<!-- 抖小云 -->
<!-- </div> -->
<div class="front-content ">
<router-view /> <router-view />
</div> </div>
</div> </div>
</ThemeProvider> </ThemeProvider>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.front-view { .front-view {
height: 100vh;
width: 100vw;
overflow: hidden;
.front-content {
height: 100vh;
overflow: auto; // 防止mobile页面内容溢出
}
// 原样式保留(无冲突)
.front-header { .front-header {
.front-nav-item { .front-nav-item {
&.with-list .front-nav-item-content { &.with-list .front-nav-item-content {
@@ -55,11 +117,5 @@ onMounted(() => {
} }
} }
} }
.front-content {
height: 100vh;
}
.front-view {
height: 100vh;
}
} }
</style> </style>
+45 -21
View File
@@ -3,54 +3,78 @@
<login-box class="shadow-lg" @success="onLoginSuccess" @failure="onLoginFail" /> <login-box class="shadow-lg" @success="onLoginSuccess" @failure="onLoginFail" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import LoginBox from './LoginBox.vue'; import LoginBox from './LoginBox.vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
// import http from '@/store/http'; import { message } from 'ant-design-vue';
const router = useRouter(); const router = useRouter();
function onLoginSuccess() { function onLoginSuccess() {
router.push('/dashboard'); router.push('/dashboard');
} }
import { message } from 'ant-design-vue';
function onLoginFail(status, res) { function onLoginFail(reason: string, fields: any) {
console.log(res); console.log('登录失败:', reason, fields);
if (res.code === 0) { message.error(reason || '登录失败,请重试', 5);
} else {
message.error(res.erro, 5);
}
} }
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.login { .login {
height: 100vh; height: 100vh;
// 与深色登录框搭配的渐变背景 min-height: -webkit-fill-available; /* 适配iOS安全区域 */
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
position: relative;
overflow: hidden; /* 防止装饰元素溢出 */
padding: 20px 0; /* 移动端上下内边距,避免卡片贴边 */
// 可以添加一些装饰性背景元素 // 装饰性背景元素 - 移动端适配尺寸
&::before { &::before {
content: ''; content: '';
position: absolute; position: absolute;
width: 400px; width: 280px;
height: 400px; height: 280px;
border-radius: 50%; border-radius: 50%;
background: rgba(54, 191, 250, 0.1); background: rgba(54, 191, 250, 0.1);
top: 20%; top: 15%;
left: 15%; left: 5%;
filter: blur(80px); filter: blur(60px);
@media (min-width: 768px) {
width: 400px;
height: 400px;
top: 20%;
left: 15%;
filter: blur(80px);
}
} }
&::after { &::after {
content: ''; content: '';
position: absolute; position: absolute;
width: 300px; width: 220px;
height: 300px; height: 220px;
border-radius: 50%; border-radius: 50%;
background: rgba(54, 191, 250, 0.08); background: rgba(54, 191, 250, 0.08);
bottom: 10%; bottom: 5%;
right: 10%; right: 5%;
filter: blur(60px); filter: blur(40px);
@media (min-width: 768px) {
width: 300px;
height: 300px;
bottom: 10%;
right: 10%;
filter: blur(60px);
}
} }
} }
</style>
/* 适配iOS安全区域 */
@supports (bottom: env(safe-area-inset-bottom)) {
.login {
padding-bottom: env(safe-area-inset-bottom);
padding-top: env(safe-area-inset-top);
}
}
</style>
+151 -102
View File
@@ -1,78 +1,70 @@
<template> <template>
<div class="login-page min-h-screen flex items-center justify-center p-4 md:p-8"> <ThemeProvider :color="{
<ThemeProvider :color="{ middle: { 'bg-base': '#fff' },
middle: { 'bg-base': '#fff' }, primary: { DEFAULT: '#4f46e5', hover: '#4338ca' }
primary: { DEFAULT: '#4f46e5', hover: '#4338ca' } }">
}"> <!-- 登录卡片 - 移动端全屏 + PC端保留原宽度 -->
<!-- 登录卡片 --> <div class="login-box rounded-2xl shadow-lg p-6 md:p-8 lg:p-10 max-w-md w-full border border-gray-200 transition-all duration-500 hover:shadow-xl transform hover:-translate-y-1 mx-auto" style="box-sizing: border-box; max-width: 90vw;">
<div class="login-box rounded-2xl shadow-lg p-8 md:p-10 max-w-md w-full border border-gray-200 transition-all duration-500 hover:shadow-xl transform hover:-translate-y-1"> <!-- 顶部Logo区域 - 边框圈精准贴合Logo内容 -->
<!-- 顶部Logo区域 --> <div class="flex flex-col items-center mb-6 md:mb-8">
<div class="flex flex-col items-center mb-8"> <div class="w-30 h-30 md:w-36 md:h-36 bg-primary/10 rounded-full flex items-center justify-center mb-4 md:mb-5">
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mb-4"> <!-- 缩小边框容器尺寸适配img实际内容 style="border: 1.5px solid rgba(79, 70, 229, 0.2); border-radius: 50%;"-->
<img src="/public/logo.png"> <div class="w-24 h-24 md:w-28 h-28 rounded-full flex items-center justify-center">
<img src="/logo.png" alt="抖小云logo" class="w-18 h-18 md:w-20 h-20 object-contain" />
</div> </div>
<h2 class="third-title text-2xl font-bold text-gray-800 tracking-tight">抖小云</h2> </div>
<h2 class="third-title text-xl md:text-2xl font-bold text-gray-800 tracking-tight">抖小云</h2>
</div>
<a-form :model="form" :wrapperCol="{ span: 24 }" @finish="login" class="login-form w-full text-gray-700">
<!-- 用户名输入框 -->
<a-form-item :required="true" name="username" class="mb-4 md:mb-5" :validate-status="usernameStatus">
<a-input v-model:value="form.username" autocomplete="new-username" placeholder="请输入用户名" class="login-input h-[46px] md:h-[50px] rounded-lg bg-gray-50 border-gray-200 text-gray-800 placeholder:text-gray-400 focus:border-primary focus:bg-white text-base transition-all duration-300 focus:ring-2 focus:ring-primary/20" style="padding: 0 12px;">
<template #prefix>
<svg class="w-4 h-4 md:w-5 md:h-5 text-gray-400 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
</template>
</a-input>
</a-form-item>
<!-- 密码输入框 -->
<a-form-item :required="true" name="password" class="mb-2" :validate-status="passwordStatus">
<a-input v-model:value="form.password" autocomplete="new-password" placeholder="请输入密码" class="login-input h-[46px] md:h-[50px] rounded-lg bg-gray-50 border-gray-200 text-gray-800 placeholder:text-gray-400 focus:border-primary focus:bg-white text-base transition-all duration-300 focus:ring-2 focus:ring-primary/20" :type="showPassword ? 'text' : 'password'" style="padding: 0 12px;">
<template #prefix>
<svg class="w-4 h-4 md:w-5 md:h-5 text-gray-400 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</template>
<!-- 密码显示/隐藏图标 -->
<template #suffix>
<div class="w-8 h-8 flex items-center justify-center" @click="showPassword = !showPassword">
<EyeOutlined v-if="showPassword" class="text-gray-400 hover:text-primary cursor-pointer transition-colors duration-300 text-lg" aria-label="隐藏密码" />
<EyeInvisibleOutlined v-else class="text-gray-400 hover:text-primary cursor-pointer transition-colors duration-300 text-lg" aria-label="显示密码" />
</div>
</template>
</a-input>
</a-form-item>
<!-- 记住密码 -->
<div class="flex items-center justify-between mb-5 md:mb-6 px-1">
<a-form-item class="mb-0">
<a-checkbox v-model:checked="rememberPassword" class="text-gray-600 hover:text-primary transition-colors duration-300 cursor-pointer">
<span class="text-xs md:text-sm">记住密码</span>
</a-checkbox>
</a-form-item>
</div> </div>
<a-form :model="form" :wrapperCol="{ span: 24 }" @finish="login" class="login-form w-full text-gray-700"> <!-- 登录按钮 -->
<!-- 用户名输入框 --> <a-button htmlType="submit" class="h-[48px] md:h-[52px] w-full rounded-lg transition-all duration-300 hover:bg-primary/90 bg-primary border-primary text-white text-base font-medium shadow-md hover:shadow-lg transform hover:-translate-y-0.5 active:translate-y-0" type="primary" :loading="loading" style="touch-action: manipulation; -webkit-tap-highlight-color: transparent;">
<a-form-item :required="true" name="username" class="mb-5" :validate-status="usernameStatus"> <span v-if="!loading">登录</span>
<a-input v-model:value="form.username" autocomplete="new-username" placeholder="请输入用户名" class="login-input h-[50px] rounded-lg bg-gray-50 border-gray-200 text-gray-800 placeholder:text-gray-400 focus:border-primary focus:bg-white text-base transition-all duration-300 focus:ring-2 focus:ring-primary/20"> <span v-else>登录中...</span>
<template #prefix> </a-button>
<svg class="w-5 h-5 text-gray-400 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> </a-form>
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path> </div>
<circle cx="12" cy="7" r="4"></circle> </ThemeProvider>
</svg>
</template>
</a-input>
</a-form-item>
<!-- 密码输入框 -->
<a-form-item :required="true" name="password" class="mb-2" :validate-status="passwordStatus">
<a-input v-model:value="form.password" autocomplete="new-password" placeholder="请输入密码" class="login-input h-[50px] rounded-lg bg-gray-50 border-gray-200 text-gray-800 placeholder:text-gray-400 focus:border-primary focus:bg-white text-base transition-all duration-300 focus:ring-2 focus:ring-primary/20" :type="showPassword ? 'text' : 'password'">
<template #prefix>
<svg class="w-5 h-5 text-gray-400 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</template>
<!-- 密码显示/隐藏图标 -->
<template #suffix>
<EyeOutlined v-if="showPassword" @click="showPassword = !showPassword" class="text-gray-400 hover:text-primary cursor-pointer transition-colors duration-300 text-lg" aria-label="隐藏密码" />
<EyeInvisibleOutlined v-else @click="showPassword = !showPassword" class="text-gray-400 hover:text-primary cursor-pointer transition-colors duration-300 text-lg" aria-label="显示密码" />
</template>
</a-input>
</a-form-item>
<!-- 记住密码与忘记密码 -->
<div class="flex items-center justify-between mb-6 px-1">
<a-form-item class="mb-0">
<a-checkbox v-model:checked="rememberPassword" class="text-gray-600 hover:text-primary transition-colors duration-300 cursor-pointer">
<span class="text-sm">记住密码</span>
</a-checkbox>
</a-form-item>
<!-- <a href="#" class="text-primary hover:text-primary/80 text-sm font-medium transition-colors duration-300" @click.prevent="handleForgotPassword">
忘记密码
</a> -->
</div>
<!-- 登录按钮 -->
<a-button htmlType="submit" class="h-[52px] w-full rounded-lg transition-all duration-300 hover:bg-primary/90 bg-primary border-primary text-white text-base font-medium shadow-md hover:shadow-lg transform hover:-translate-y-0.5 active:translate-y-0" type="primary" :loading="loading">
<span v-if="!loading">登录</span>
<span v-else>登录中...</span>
</a-button>
<!-- 注册入口 -->
<!-- <div class="text-center mt-6 text-gray-500 text-sm">
还没有账号
<a href="#" class="text-primary hover:text-primary/80 font-medium transition-colors duration-300 ml-1" @click.prevent="handleRegister">
立即注册
</a>
</div> -->
</a-form>
</div>
</ThemeProvider>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -90,6 +82,10 @@ const rememberPassword = ref(false);
const loading = ref(false); const loading = ref(false);
// 表单状态(用于输入框验证反馈) // 表单状态(用于输入框验证反馈)
const form = reactive({
username: '',
password: '',
});
const usernameStatus = computed(() => (form.username ? 'success' : '')); const usernameStatus = computed(() => (form.username ? 'success' : ''));
const passwordStatus = computed(() => (form.password ? 'success' : '')); const passwordStatus = computed(() => (form.password ? 'success' : ''));
@@ -98,11 +94,6 @@ export interface LoginFormProps {
password: string; password: string;
} }
const form = reactive<LoginFormProps>({
username: '',
password: '',
});
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'success', fields: LoginFormProps): void; (e: 'success', fields: LoginFormProps): void;
(e: 'failure', reason: string, fields: LoginFormProps): void; (e: 'failure', reason: string, fields: LoginFormProps): void;
@@ -161,8 +152,8 @@ async function login(params: LoginFormProps) {
console.log(res); console.log(res);
message.success('登录成功!'); message.success('登录成功!');
} catch (e: any) { } catch (e: any) {
// emit('failure', e.message, e.data); emit('failure', e.message || '登录失败', params);
message.error(e.data.erro || '登录失败,请重试'); message.error(e.data?.erro || '登录失败,请重试');
} finally { } finally {
loading.value = false; loading.value = false;
} }
@@ -181,15 +172,85 @@ function handleRegister() {
</script> </script>
<style scoped> <style scoped>
/* 页面基础样式 */ /* 全局布局适配 - 替代原外层容器的样式 */
.login-page { :root {
-webkit-text-size-adjust: 100%; /* 禁止iOS文本缩放 */
-webkit-font-smoothing: antialiased; /* 优化字体渲染 */
}
body {
margin: 0;
padding: 0;
overflow-x: hidden;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
/* 全局居中布局 */
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
}
/* 登录卡片核心样式 */
.login-box {
box-sizing: border-box;
touch-action: manipulation; /* 优化触摸性能 */
/* 移动端全屏,PC端保留原max-w-md(默认28rem/448px+ 90vw限制 */
}
/* 移动端适配(<768px */
@media (max-width: 767.98px) {
body {
padding: 0;
}
.login-box {
max-width: 100vw !important; /* 移动端全屏 */
min-height: 100vh;
border: none !important;
border-radius: 0 !important;
box-shadow: none !important;
padding: 20px 16px !important;
padding-top: 20vh !important; /* 移动端垂直居中 */
}
.third-title {
font-size: 1.25rem !important;
}
::v-deep(.ant-input) {
font-size: 14px; /* 移动端字体适配 */
}
}
/* 小屏手机额外适配 */
@media (max-width: 480px) {
.login-box {
padding-top: 15vh !important;
}
}
/* PC端样式(保留原有宽度,仅调小最大宽度) */
@media (min-width: 768px) {
body {
padding: 8px;
}
.login-box {
/* 原宽度是 min(448px, 90vw),现调整为更小的数值,比如 380px(可根据需求修改) */
max-width: min(340px, 90vw) !important;
min-height: auto !important;
border: 1px solid #e5e7eb !important;
border-radius: 1rem !important;
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1) !important;
padding-top: 0 !important;
}
} }
/* 输入框聚焦动画优化 */ /* 输入框聚焦动画优化 */
::v-deep(.ant-input) {
box-sizing: border-box;
}
::v-deep(.ant-input:focus) { ::v-deep(.ant-input:focus) {
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2) !important; box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2) !important;
border-color: #4f46e5 !important; border-color: #4f46e5 !important;
outline: none; /* 移除默认轮廓 */
} }
/* 复选框样式优化 */ /* 复选框样式优化 */
@@ -197,29 +258,26 @@ function handleRegister() {
background-color: #4f46e5 !important; background-color: #4f46e5 !important;
border-color: #4f46e5 !important; border-color: #4f46e5 !important;
} }
::v-deep(.ant-checkbox:hover .ant-checkbox-inner) { ::v-deep(.ant-checkbox:hover .ant-checkbox-inner) {
border-color: #4f46e5 !important; border-color: #4f46e5 !important;
} }
::v-deep(.ant-checkbox) {
transform: scale(0.9); /* 移动端复选框适当缩小 */
}
/* 按钮样式优化 */ /* 按钮样式优化 */
::v-deep(.ant-btn-primary) { ::v-deep(.ant-btn-primary) {
background-color: #4f46e5 !important; background-color: #4f46e5 !important;
border-color: #4f46e5 !important; border-color: #4f46e5 !important;
box-sizing: border-box;
} }
::v-deep(.ant-btn-primary:hover) { ::v-deep(.ant-btn-primary:hover) {
background-color: #4338ca !important; background-color: #4338ca !important;
border-color: #4338ca !important; border-color: #4338ca !important;
} }
::v-deep(.ant-btn-primary:focus) { ::v-deep(.ant-btn-primary:focus) {
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.3) !important; box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.3) !important;
} outline: none;
/* 分割线样式优化 */
::v-deep(.ant-divider) {
background-color: #e5e7eb !important;
} }
/* 加载状态动画优化 */ /* 加载状态动画优化 */
@@ -227,24 +285,15 @@ function handleRegister() {
margin-right: 8px !important; margin-right: 8px !important;
} }
/* 响应式调整 */ /* 防止点击闪烁和触摸高亮 */
@media (max-width: 375px) { * {
.login-box { -webkit-tap-highlight-color: transparent;
padding: 60px 20px !important; tap-highlight-color: transparent;
}
.third-title {
font-size: 1.5rem !important;
}
} }
/* 平滑滚动 */ /* 平滑滚动 */
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
} overflow-x: hidden; /* 防止移动端横向滚动 */
/* 防止点击闪烁 */
* {
-webkit-tap-highlight-color: transparent;
} }
</style> </style>
+999
View File
@@ -0,0 +1,999 @@
<template>
<div class="stats-dashboard-mobile">
<div class="dashboard-container">
<!-- 看板 Tab 内容 -->
<div v-if="activeTab === 'dashboard'" class="tab-content">
<!-- 仅显示核心统计概览无底部详细数据 -->
<section class="stats-overview">
<!-- 总视频数卡片 -->
<div class="stat-card primary-card main-card">
<div class="stat-header">
<div class="header-left">
<span class="stat-meta">视频总数</span>
<div class="stat-value">{{ totalVideos }}</div>
</div>
<div class="stat-icon video-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="23 7 16 12 23 17 23 7"></polygon>
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
</svg>
</div>
</div>
<div class="stat-subitems">
<div class="subitem" :title="`我喜欢的视频数: ${favoriteCount}`">
<div class="subitem-icon like-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
</svg>
</div>
<span class="subitem-meta">我喜欢的</span>
<span class="subitem-value">{{ favoriteCount }}</span>
</div>
<div class="subitem" :title="`我收藏的视频数: ${collectCount}`">
<div class="subitem-icon collect-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
</svg>
</div>
<span class="subitem-meta">我收藏的</span>
<span class="subitem-value">{{ collectCount }}</span>
</div>
<div class="subitem" :title="`我关注的视频数: ${followCount}`">
<div class="subitem-icon follow-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="8.5" cy="7" r="4"></circle>
<line x1="20" y1="8" x2="20" y2="14"></line>
<line x1="23" y1="11" x2="17" y2="11"></line>
</svg>
</div>
<span class="subitem-meta">我关注的</span>
<span class="subitem-value">{{ followCount }}</span>
</div>
<div class="subitem" :title="`图文视频数: ${graphicVideoCount}`">
<div class="subitem-icon graphic-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
<span class="subitem-meta">图文视频</span>
<span class="subitem-value">{{ graphicVideoCount }}</span>
</div>
</div>
</div>
<!-- 总占用空间卡片 -->
<div class="stat-card secondary-card main-card">
<div class="stat-header">
<div class="header-left">
<span class="stat-meta">空间总计</span>
<div class="stat-value">{{ fileSizeTotal }} <span class="unit">G</span></div>
</div>
<div class="stat-icon size-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 12H2v8h20v-8z" />
<path d="M6 18h.01" />
<path d="M10 18h.01" />
</svg>
</div>
</div>
<div class="stat-subitems">
<div class="subitem" :title="`喜欢的视频占用: ${favoriteSize}G`">
<div class="subitem-icon like-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
</svg>
</div>
<span class="subitem-meta">喜欢占用</span>
<span class="subitem-value">{{ favoriteSize }} <span class="unit">G</span></span>
</div>
<div class="subitem" :title="`收藏的视频占用: ${collectSize}G`">
<div class="subitem-icon collect-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
</svg>
</div>
<span class="subitem-meta">收藏占用</span>
<span class="subitem-value">{{ collectSize }} <span class="unit">G</span></span>
</div>
<div class="subitem" :title="`关注的视频占用: ${followSize}G`">
<div class="subitem-icon follow-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="8.5" cy="7" r="4"></circle>
<line x1="20" y1="8" x2="20" y2="14"></line>
<line x1="23" y1="11" x2="17" y2="11"></line>
</svg>
</div>
<span class="subitem-meta">关注占用</span>
<span class="subitem-value">{{ followSize }} <span class="unit">G</span></span>
</div>
<div class="subitem" :title="`图文视频占用: ${graphicVideoSize}G`">
<div class="subitem-icon graphic-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
<span class="subitem-meta">图文占用</span>
<span class="subitem-value">{{ graphicVideoSize }} <span class="unit">G</span></span>
</div>
</div>
</div>
</section>
</div>
<!-- 日志 Tab 内容 -->
<div v-if="activeTab === 'log'" class="tab-content log-tab">
<div class="log-header">
<h2 class="log-title">系统日志</h2>
<div class="log-filter">
<button class="filter-btn" :class="{ active: logFilter === 'all' }" @click="logFilter = 'all'">
全部
</button>
<button class="filter-btn" :class="{ active: logFilter === 'debug' }" @click="logFilter = 'debug'">
DEBUG
</button>
<button class="filter-btn" :class="{ active: logFilter === 'error' }" @click="logFilter = 'error'">
ERROR
</button>
</div>
</div>
<div class="log-list">
<div v-for="(log,index) in filteredLogs" :key="index" class="log-item" :class="log.type.toLowerCase()" @click="openLogDetail(log)">
<div class="log-item-header">
<span class="log-type" :class="log.type.toLowerCase()">
{{ log.type }}
</span>
<span class="log-time">{{formatShowDate(log?.date) }}</span>
</div>
<!-- <div class="log-message">{{ log.message }}</div> -->
<div class="log-file">{{ log.fileName }}</div>
</div>
<div v-if="filteredLogs.length === 0" class="empty-log">
暂无{{ logFilter === 'all' ? '' : logFilter.toUpperCase() }}类型日志
</div>
</div>
</div>
</div>
<!-- 底部导航菜单 -->
<div class="bottom-nav">
<div class="nav-item" :class="{ active: activeTab === 'dashboard' }" @click="activeTab = 'dashboard'">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="3 11 12 2 21 11 12 20 3 11"></polygon>
<line x1="12" y1="2" x2="12" y2="20"></line>
</svg>
<span>看板</span>
</div>
<div class="nav-item" :class="{ active: activeTab === 'log' }" @click="activeTab = 'log'">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
<span>日志</span>
</div>
</div>
<!-- 日志详情弹窗 -->
<div v-if="showLogModal" class="log-modal-mask" @click="closeLogModal">
<div class="log-modal-content" @click.stop>
<!-- 弹窗头部 -->
<div class="log-modal-header">
<div class="header-left">
<span class="modal-type-tag" :class="currentLog?.type.toLowerCase()">
{{ currentLog?.type }}
</span>
<span class="modal-date">{{ formatShowDate(currentLog?.date) }}</span>
</div>
<button class="close-btn" @click="closeLogModal">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<!-- 弹窗内容 -->
<div class="log-modal-body">
<div class="file-name">{{ currentLog?.fileName }}</div>
<pre class="log-content">{{ logContent || '加载日志内容中...' }}</pre>
</div>
<!-- 弹窗底部 -->
<div class="log-modal-footer">
<button class="copy-btn" @click="copyLogContent" :disabled="!logContent">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
复制日志
</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, computed } from 'vue';
import { useApiStore } from '@/store';
import { message } from 'ant-design-vue';
// 类型接口
interface Author {
name: string;
count: number;
icon: string;
}
interface Category {
name: string;
count: number;
color: string;
icon: string;
}
interface LogItem {
id: string;
type: 'DEBUG' | 'ERROR';
time: string;
message: string;
file: string;
date: string;
fileName: string;
}
// 状态管理(仅保留核心数据)
const totalVideos = ref<number>(0);
const fileSizeTotal = ref<string>('0.00');
const favoriteCount = ref<number>(0);
const collectCount = ref<number>(0);
const followCount = ref<number>(0);
const graphicVideoCount = ref<number>(0);
const favoriteSize = ref<string>('0.00');
const collectSize = ref<string>('0.00');
const followSize = ref<string>('0.00');
const graphicVideoSize = ref<string>('0.00');
// 导航相关状态
const activeTab = ref<string>('dashboard');
const logFilter = ref<string>('all');
const logs = ref<LogItem[]>([]);
// 弹窗相关状态
const showLogModal = ref<boolean>(false);
const currentLog = ref<LogItem | null>(null);
const logContent = ref<string>('');
// 过滤后的日志列表
const filteredLogs = computed(() => {
if (logFilter.value === 'all') {
return logs.value;
}
return logs.value.filter((log) => log.type.toLowerCase() === logFilter.value);
});
// 组件名称
defineOptions({ name: 'StatsDashboardMobile' });
// 加载核心数据
onMounted(() => {
loadDashboardData();
loadLogData();
});
const loadDashboardData = async () => {
try {
const res = await useApiStore().VideoStatics();
totalVideos.value = res.data.videoCount;
fileSizeTotal.value = res.data.videoSizeTotal || '0.00';
favoriteCount.value = res.data.favoriteCount;
collectCount.value = res.data.collectCount;
followCount.value = res.data.followCount || 0;
graphicVideoCount.value = res.data.graphicVideoCount || 0;
favoriteSize.value = res.data.videoFavoriteSize || '0.00';
collectSize.value = res.data.videoCollectSize || '0.00';
followSize.value = res.data.videoFollowSize || '0.00';
graphicVideoSize.value = res.data.graphicVideoSize || '0.00';
} catch (err) {
console.error('加载移动端仪表盘数据失败:', err);
}
};
// 加载日志数据(模拟接口,实际项目中替换为真实接口)
const loadLogData = async () => {
try {
// 模拟日志数据,实际项目中替换为真实接口调用
useApiStore()
.MobileLogs()
.then((res) => {
if (res.code == 0) {
logs.value = res.data;
}
});
} catch (err) {
console.error('加载日志数据失败:', err);
}
};
// 打开日志详情弹窗
const openLogDetail = (log: LogItem) => {
currentLog.value = log;
showLogModal.value = true;
// 加载日志详情内容
loadLogDetailContent(log);
};
// 关闭日志详情弹窗
const closeLogModal = () => {
showLogModal.value = false;
currentLog.value = null;
logContent.value = '';
};
// 加载日志详情内容
const loadLogDetailContent = (log: LogItem) => {
try {
const type = log.type.toLowerCase();
const date = log.date;
const requestParams = `type=${type}&date=${date}`;
useApiStore()
.apiGetLogs(requestParams)
.then((logContentStr: string) => {
// 处理时间戳,仅保留时分秒
const formattedLog = formatLogTime(logContentStr);
const lines = formattedLog.split('\n'); // 将文本按换行符分割为行数组
const reversedLines = lines.reverse(); // 对行数组进行倒序操作
const reversedText = reversedLines.join('\n'); // 将行数组重新连接为一个字符串
logContent.value = reversedText;
})
.catch((err) => {
console.error('加载日志详情失败:', err);
logContent.value = '日志内容加载失败,请重试';
});
} catch (err) {
console.error('加载日志详情失败:', err);
logContent.value = '日志内容加载失败,请重试';
}
};
// 格式化日期显示(20251211 → 2025-12-11
const formatShowDate = (dateStr?: string) => {
if (!dateStr || dateStr.length !== 8) return dateStr || '';
return `${dateStr.slice(0, 4)}-${dateStr.slice(4, 6)}-${dateStr.slice(6, 8)}`;
};
// 处理日志时间戳,仅保留时分秒
const formatLogTime = (logContent: string) => {
const timeRegex = /\d{4}-\d{2}-\d{2} (\d{2}:\d{2}:\d{2})\.\d+ \+08:00/g;
return logContent.replace(timeRegex, '$1');
};
// 复制日志内容
const copyLogContent = () => {
if (!logContent.value) {
message.warn('暂无日志内容可复制');
return;
}
// 移动端复制兼容
if (navigator.clipboard) {
navigator.clipboard
.writeText(logContent.value)
.then(() => message.success('日志内容已复制到剪贴板'))
.catch(() => {
fallbackCopyTextToClipboard(logContent.value);
});
} else {
fallbackCopyTextToClipboard(logContent.value);
}
};
// 复制降级方案
const fallbackCopyTextToClipboard = (text: string) => {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
const msg = successful ? '日志内容已复制到剪贴板' : '复制失败,请手动复制';
message.success(msg);
} catch (err) {
console.error('Fallback: Oops, unable to copy', err);
message.error('复制失败,请手动复制');
}
document.body.removeChild(textArea);
};
</script>
<style scoped>
/* 移动端专属样式 */
.stats-dashboard-mobile {
min-height: 100vh;
background-color: #ffffff;
color: #333333;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 15px 0;
display: flex;
flex-direction: column;
padding-bottom: 80px; /* 为底部导航留出空间 */
position: relative;
}
.dashboard-container {
width: 100%;
padding: 0 15px;
box-sizing: border-box;
flex: 1;
overflow-y: auto;
}
.tab-content {
width: 100%;
height: 100%;
}
.stats-overview {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
margin-bottom: 20px;
}
.main-card {
padding: 20px 16px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
border: 1px solid #f0f0f0;
background: #fff;
}
.main-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.08);
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.header-left {
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-meta {
font-size: 14px;
color: #666666;
text-transform: uppercase;
letter-spacing: 0.4px;
font-weight: 500;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #1a1a1a;
line-height: 1.1;
display: flex;
align-items: baseline;
gap: 6px;
}
.unit {
font-size: 18px;
color: #444444;
font-weight: 500;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background-color: rgba(76, 175, 80, 0.15);
color: #4caf50;
}
.secondary-card .stat-icon {
background-color: rgba(33, 150, 243, 0.15);
color: #2196f3;
}
.stat-subitems {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
.subitem {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
background: #ffffff;
border: 1px solid #f0f0f0;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: all 0.2s ease;
cursor: default;
}
.subitem:hover {
transform: translateY(-1px);
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.06);
border-color: #e0e0e0;
}
.subitem-icon {
width: 28px;
height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background-color: rgba(233, 30, 99, 0.1);
color: #e91e63;
}
.collect-icon {
background-color: rgba(255, 152, 0, 0.1);
color: #ff9800;
}
.follow-icon {
background-color: rgba(156, 39, 176, 0.1);
color: #9c27b0;
}
.graphic-icon {
background-color: rgba(255, 159, 64, 0.1);
color: #d9091a;
}
.subitem-meta {
font-size: 13px;
color: #666666;
font-weight: 500;
flex: 1;
}
.subitem-value {
font-size: 15px;
font-weight: 600;
color: #222222;
display: flex;
align-items: baseline;
gap: 3px;
}
.subitem-value .unit {
font-size: 12px;
color: #555555;
font-weight: 500;
}
.primary-card {
border-top: 3px solid #4caf50;
}
.secondary-card {
border-top: 3px solid #2196f3;
}
/* 底部导航样式 */
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 60px;
background-color: #ffffff;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: space-around;
align-items: center;
z-index: 100;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
}
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
height: 100%;
color: #666666;
transition: all 0.2s ease;
cursor: pointer;
}
.nav-item.active {
color: #4caf50;
}
.nav-item span {
font-size: 12px;
margin-top: 4px;
font-weight: 500;
}
.nav-item svg {
transition: all 0.2s ease;
}
.nav-item.active svg {
transform: scale(1.1);
}
/* 日志 Tab 样式 */
.log-tab {
padding: 10px 0;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 0 5px;
}
.log-title {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin: 0;
}
.log-filter {
display: flex;
gap: 8px;
}
.filter-btn {
padding: 6px 12px;
border-radius: 20px;
border: 1px solid #e0e0e0;
background-color: #ffffff;
color: #666666;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.filter-btn.active {
background-color: #4caf50;
color: #ffffff;
border-color: #4caf50;
}
.filter-btn:hover:not(.active) {
border-color: #cccccc;
color: #333333;
}
/* 日志列表样式 */
.log-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.log-item {
padding: 15px;
border-radius: 8px;
border: 1px solid #f0f0f0;
background-color: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
cursor: pointer;
transition: all 0.2s ease;
}
.log-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.log-item.debug {
border-left: 4px solid #2196f3;
}
.log-item.error {
border-left: 4px solid #f44336;
}
.log-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.log-type {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.log-type.debug {
background-color: rgba(33, 150, 243, 0.1);
color: #2196f3;
}
.log-type.error {
background-color: rgba(244, 67, 54, 0.1);
color: #f44336;
}
.log-time {
font-size: 11px;
color: #999999;
}
.log-message {
font-size: 14px;
color: #333333;
font-weight: 500;
margin-bottom: 6px;
line-height: 1.4;
}
.log-file {
font-size: 12px;
color: #666666;
}
.empty-log {
text-align: center;
padding: 40px 20px;
color: #999999;
font-size: 14px;
}
/* 日志详情弹窗样式 */
.log-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
box-sizing: border-box;
}
.log-modal-content {
width: 100%;
max-width: 500px;
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
max-height: 80vh;
display: flex;
flex-direction: column;
}
.log-modal-header {
padding: 10px;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left {
display: flex;
align-items: center;
gap: 8px;
}
.modal-type-tag {
padding: 3px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.modal-type-tag.debug {
background-color: rgba(33, 150, 243, 0.1);
color: #2196f3;
}
.modal-type-tag.error {
background-color: rgba(244, 67, 54, 0.1);
color: #f44336;
}
.modal-date {
font-size: 14px;
color: #666;
font-weight: 500;
}
.close-btn {
background: transparent;
border: none;
cursor: pointer;
color: #666;
padding: 4px;
border-radius: 4px;
transition: all 0.2s ease;
}
.close-btn:hover {
background-color: #f5f5f5;
color: #333;
}
.log-modal-body {
padding: 10px;
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.file-name {
font-size: 14px;
color: #333;
font-weight: 600;
margin-bottom: 5px;
/* padding-bottom: 8px; */
border-bottom: 1px solid #f0f0f0;
}
.log-content {
flex: 1;
margin: 0;
padding: 5px;
background-color: #f9f9f9;
border-radius: 8px;
overflow-y: auto;
overflow-x: auto;
font-family: Consolas, Monaco, monospace;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
color: #333;
}
.log-modal-footer {
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
}
.copy-btn {
padding: 8px 16px;
background-color: #4caf50;
color: #ffffff;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
transition: all 0.2s ease;
}
.copy-btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.copy-btn:hover:not(:disabled) {
background-color: #43a047;
}
/* 夜间模式 */
html.dark-mode .stats-dashboard-mobile {
background-color: #1a1a2e;
color: #eaeaea;
padding-bottom: 80px;
}
html.dark-mode .main-card {
border-color: rgba(255, 255, 255, 0.1);
background-color: rgba(30, 30, 50, 0.9);
}
html.dark-mode .stat-subitems {
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
html.dark-mode .subitem {
background: rgba(40, 40, 65, 0.7);
border: 1px solid rgba(255, 255, 255, 0.05);
}
html.dark-mode .subitem:hover {
background: rgba(40, 40, 65, 0.9);
border-color: rgba(255, 255, 255, 0.1);
}
html.dark-mode .stat-meta {
color: #b0b0c3;
}
html.dark-mode .stat-value {
color: #ffffff;
}
html.dark-mode .unit {
color: #d0d0d0;
}
html.dark-mode .stat-icon {
background-color: rgba(76, 175, 80, 0.25);
}
html.dark-mode .secondary-card .stat-icon {
background-color: rgba(33, 150, 243, 0.25);
}
html.dark-mode .subitem-meta {
color: #c0c0d3;
}
html.dark-mode .subitem-value {
color: #ffffff;
}
html.dark-mode .subitem-value .unit {
color: #b0b0c3;
}
html.dark-mode .subitem-icon {
background-color: rgba(233, 30, 99, 0.2);
}
html.dark-mode .collect-icon {
background-color: rgba(255, 152, 0, 0.2);
}
html.dark-mode .follow-icon {
background-color: rgba(156, 39, 176, 0.2);
}
html.dark-mode .graphic-icon {
background-color: rgba(255, 159, 64, 0.2);
}
/* 夜间模式 - 底部导航 */
html.dark-mode .bottom-nav {
background-color: #16213e;
border-top-color: rgba(255, 255, 255, 0.1);
}
html.dark-mode .nav-item {
color: #b0b0c3;
}
html.dark-mode .nav-item.active {
color: #4caf50;
}
/* 夜间模式 - 日志样式 */
html.dark-mode .log-title {
color: #ffffff;
}
html.dark-mode .filter-btn {
background-color: #1f4068;
border-color: rgba(255, 255, 255, 0.1);
color: #e0e0e0;
}
html.dark-mode .filter-btn.active {
background-color: #4caf50;
color: #ffffff;
border-color: #4caf50;
}
html.dark-mode .log-item {
background-color: rgba(30, 30, 50, 0.9);
border-color: rgba(255, 255, 255, 0.05);
}
html.dark-mode .log-message {
color: #eaeaea;
}
html.dark-mode .log-file {
color: #c0c0d3;
}
html.dark-mode .empty-log {
color: #808099;
}
/* 夜间模式 - 弹窗样式 */
html.dark-mode .log-modal-content {
background-color: #1e1e3f;
border-color: rgba(255, 255, 255, 0.1);
}
html.dark-mode .log-modal-header,
html.dark-mode .log-modal-footer {
border-color: rgba(255, 255, 255, 0.1);
}
html.dark-mode .modal-date {
color: #c0c0d3;
}
html.dark-mode .file-name {
color: #ffffff;
border-color: rgba(255, 255, 255, 0.1);
}
html.dark-mode .log-content {
background-color: #2a2a4a;
color: #eaeaea;
}
html.dark-mode .close-btn {
color: #c0c0d3;
}
html.dark-mode .close-btn:hover {
background-color: #2a2a4a;
color: #ffffff;
}
.ant-message {
z-index: 10000 !important;
}
</style>
+13
View File
@@ -45,6 +45,18 @@ const typeChange = (e) => {
loadLogs(); loadLogs();
}; };
const mIfrm = ref<any>(null); const mIfrm = ref<any>(null);
// 核心:处理日志时间戳,移除 .xxx +08:00 部分
const formatLogTime = (logContent: string) => {
// 正则匹配:2025-12-11 18:41:39.624 +08:00 格式,捕获前面的日期时间部分
// 正则解释:
// (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) 捕获 年-月-日 时:分:秒
// \.\d+ \+08:00 匹配 .毫秒 +时区 部分
const timeRegex = /(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\.\d+ \+08:00/g;
// 替换匹配到的内容,只保留捕获的日期时间部分
return logContent.replace(timeRegex, '$1');
};
onMounted(() => { onMounted(() => {
// console.log(mIfrm.value); // console.log(mIfrm.value);
loadLogs(); loadLogs();
@@ -54,6 +66,7 @@ const loadLogs = () => {
.apiGetLogs(iframeUrl.value) .apiGetLogs(iframeUrl.value)
.then((log) => { .then((log) => {
// console.log(log); // console.log(log);
log = formatLogTime(log);
const lines = log.split('\n'); // 将文本按换行符分割为行数组 const lines = log.split('\n'); // 将文本按换行符分割为行数组
const reversedLines = lines.reverse(); // 对行数组进行倒序操作 const reversedLines = lines.reverse(); // 对行数组进行倒序操作
const reversedText = reversedLines.join('\n'); // 将行数组重新连接为一个字符串 const reversedText = reversedLines.join('\n'); // 将行数组重新连接为一个字符串
+1 -1
View File
@@ -16,7 +16,7 @@ interface NaviGuard {
const loginGuard: NavigationGuard = function (to, from) { const loginGuard: NavigationGuard = function (to, from) {
// console.log('Authorization', http.checkAuthorization()) // console.log('Authorization', http.checkAuthorization())
const account = useAccountStore(); const account = useAccountStore();
if (!http.checkAuthorization() && !/^\/(login|home|init)?$/.test(to.fullPath)) { if (!http.checkAuthorization() && !/^\/(login|home|init|mobile)?$/.test(to.fullPath)) {
account.setLogged(false) account.setLogged(false)
return '/login'; return '/login';
} else { } else {
+25 -1
View File
@@ -14,6 +14,18 @@ const routes: RouteRecordRaw[] = [
children: null, children: null,
component: () => import('@/pages/login'), component: () => import('@/pages/login'),
}, },
{
path: '/',
name: 'mobile',
redirect: '/mobile',
meta: {
title: '登录',
renderMenu: false,
icon: 'CreditCardOutlined',
},
children: null,
component: () => import('@/pages/mobile/MobileDashboard.vue'),
},
// { // {
// path: '/', // path: '/',
// name: 'init', // name: 'init',
@@ -26,6 +38,7 @@ const routes: RouteRecordRaw[] = [
// children: null, // children: null,
// component: () => import('@/pages/init'), // component: () => import('@/pages/init'),
// }, // },
{ {
path: '/front', path: '/front',
name: '前端', name: '前端',
@@ -45,7 +58,18 @@ const routes: RouteRecordRaw[] = [
}, },
component: () => import('@/pages/login'), component: () => import('@/pages/login'),
}, },
{
path: '/mobile',
name: 'mobile',
meta: {
icon: 'LoginOutlined',
view: 'blank',
target: '_blank',
cacheable: false,
},
children: null,
component: () => import('@/pages/mobile/MobileDashboard.vue'),
},
// { // {
// path: '/init', // path: '/init',
// name: '初始化', // name: '初始化',
+20
View File
@@ -236,7 +236,27 @@ export const useApiStore = defineStore('coreapi', () => {
}); });
} }
//移动端获取日志列表
async function MobileLogs() {
return http.request<any, Response<any>>('/api/logs/list', 'get').then(r => {
return r.data;
}).finally(() => {
});
}
//移动端获取日志详情
async function LogDetail(type: string, date: string) {
return http.request<any, Response<any>>('/api/logs/content?type=' + type + "&Date=" + date, 'get').then(r => {
return r.data;
}).finally(() => {
});
}
return { return {
LogDetail,
MobileLogs,
ExportConf, ExportConf,
ImportConf, ImportConf,
GetDeleteViedos, GetDeleteViedos,
+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"dbconn": "", "dbconn": "",
//"tagName": "arm_1.8.5", //"tagName": "arm_1.8.6",
//"tagName": "dev_1.8.5", //"tagName": "dev_1.8.6",
"tagName": "beta_1.8.5", "tagName": "beta_1.8.6",
"dbtype": "Sqlite" "dbtype": "Sqlite"
} }
+40
View File
@@ -0,0 +1,40 @@
using System.ComponentModel.DataAnnotations;
namespace dy.net.dto
{
public class LogFileInfoDto
{
/// <summary>
/// 日志类型(debug/error
/// </summary>
public string Type { get; set; }
/// <summary>
/// 日志日期(格式:20251203
/// </summary>
public string Date { get; set; }
/// <summary>
/// 完整文件名
/// </summary>
public string FileName { get; set; }
}
public class LogContentRequest
{
/// <summary>
/// 日志类型
/// </summary>
[Required]
public string Type { get; set; }
/// <summary>
/// 日志日期
/// </summary>
[Required]
public string Date { get; set; }
}
}
+83
View File
@@ -0,0 +1,83 @@
using dy.net.dto;
using System.Globalization;
namespace dy.net.service
{
public class LogInfoService
{
public List<LogFileInfoDto> GetLogFiles(string logDirectory)
{
var logFiles = new List<LogFileInfoDto>();
// 验证目录是否存在
if (!Directory.Exists(logDirectory))
{
Serilog.Log.Error("日志目录不存在:{Directory}", logDirectory);
return logFiles;
}
// 获取最近10天的日期范围
var endDate = DateTime.Today;
var startDate = endDate.AddDays(-9);
// 遍历日期范围,查找对应日志文件
for (var date = startDate; date <= endDate; date = date.AddDays(1))
{
var dateStr = date.ToString("yyyyMMdd");
// 查找debug日志
var debugFile = Path.Combine(logDirectory, $"log-debug-{dateStr}.txt");
if (File.Exists(debugFile))
{
logFiles.Add(new LogFileInfoDto
{
Type = "debug",
Date = dateStr,
FileName = Path.GetFileName(debugFile)
});
}
// 查找error日志
var errorFile = Path.Combine(logDirectory, $"log-error-{dateStr}.txt");
if (File.Exists(errorFile))
{
logFiles.Add(new LogFileInfoDto
{
Type = "error",
Date = dateStr,
FileName = Path.GetFileName(errorFile)
});
}
}
return logFiles.OrderByDescending(x => x.Date).ToList();
}
public Stream GetLogFileStream(string logDirectory, string type, string date)
{
// 验证日期格式
if (!DateTime.TryParseExact(date, "yyyyMMdd", CultureInfo.InvariantCulture,
DateTimeStyles.None, out _))
{
throw new ArgumentException("日期格式错误,应为yyyyMMdd");
}
// 验证类型
if (type != "debug" && type != "error")
{
throw new ArgumentException("日志类型只能是debug或error");
}
var filePath = Path.Combine(logDirectory, $"log-{type}-{date}.txt");
if (!File.Exists(filePath))
{
throw new FileNotFoundException("日志文件不存在", filePath);
}
// 返回文件流(使用FileStream确保流可被前端读取)
return new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
}
}
}