增加移动端,可以查看统计数据和日志
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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'); // 将行数组重新连接为一个字符串
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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: '初始化',
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user