1、nfo刮削文件优化,修复演员信息(视频作者),建议emby媒体库选影视,或者混合类型-右上角有一个兼容旧数据的按钮一键重置所有刮削文件
2、图文视频 以及动态视频,遇到因版权原因无法下载音频时,需要额外的音频文件,现在放到mp3目录了,docker-compose增加一个映射路径即可 例如: - /vol2/1000/media/dysync/mp3:/app/mp3 3、增加动态视频合成配置(不建议保留原视频,没有额外刮削的,因为有些视频是几十个很短的动图一样的视频拼接成的,保留原视频emby会出现一堆没有封面的视频) 4、容器重启后,手机端会连续跳转登录页很多次的bug修复 5、关注博主保存文件夹路径长度增加,允许_ 6、批量永久删除选中的视频(不再下载,慎重) 7、看板页面 隐藏一个功能:双击作者头像,可以删除作者所有视频(永久删除,不再下载,慎重) 8、手机端 视频播放优化
This commit is contained in:
+126
-107
@@ -4,114 +4,140 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<title>抖小云</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: #ffffff;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
font-family: "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
/* ######### 核心修改:区分 #stepin-app 的两个状态 ######### */
|
||||
/* 1. 加载阶段:#stepin-app 仅包含加载动画(或为空),应用 flex 居中,保留原有样式 */
|
||||
#stepin-app {
|
||||
/* 加载阶段默认样式:flex 居中,保留呼吸圆环效果 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 35px;
|
||||
}
|
||||
|
||||
/* 2. Vue 挂载后:#stepin-app 非空(包含 Vue 组件),覆盖为普通块级元素,解除约束 */
|
||||
#stepin-app:not(:has(.breath-ring)) {
|
||||
/* 核心:取消 flex 布局,改为普通块级元素 */
|
||||
display: block !important;
|
||||
/* 覆盖原有 flex 相关属性,解除居中约束 */
|
||||
align-items: unset !important;
|
||||
justify-content: unset !important;
|
||||
/* 确保页面撑满横向宽度 */
|
||||
width: 100% !important;
|
||||
/* 保留高度,确保 Vue 页面正常占满视口 */
|
||||
height: 100% !important;
|
||||
/* 移除 gap,避免影响 Vue 页面布局 */
|
||||
gap: 0 !important;
|
||||
/* 恢复页面滚动功能,解除 overflow: hidden 的限制 */
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
/* 核心呼吸圆环容器(原有样式保留,无修改) */
|
||||
.breath-ring {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 圆环基础样式(原有样式保留,无修改) */
|
||||
.breath-ring::before,
|
||||
.breath-ring::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 内层圆环:细圆环 + 渐变(原有样式保留,无修改) */
|
||||
.breath-ring::before {
|
||||
border: 3px solid transparent;
|
||||
border-top-color: #1890ff;
|
||||
border-right-color: #722ed1;
|
||||
animation: ringRotate 1.5s linear infinite;
|
||||
}
|
||||
|
||||
/* 外层圆环:呼吸扩散动画(原有样式保留,无修改) */
|
||||
.breath-ring::after {
|
||||
border: 2px solid #1890ff;
|
||||
animation: ringBreath 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 加载文字:极简高级(原有样式保留,无修改) */
|
||||
.loading-text {
|
||||
color: #222222;
|
||||
font-size: 17px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.loading-tip {
|
||||
color: #aaaaaa;
|
||||
font-size: 13px;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
/* 圆环旋转动画(原有样式保留,无修改) */
|
||||
@keyframes ringRotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 圆环呼吸扩散动画(原有样式保留,无修改) */
|
||||
@keyframes ringBreath {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="stepin-app">
|
||||
<style>
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
background-color: #011627;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loader {
|
||||
color: rgb(201, 195, 195);
|
||||
font-family: "Poppins", sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
-webkit-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
height: 40px;
|
||||
padding: 10px 10px;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.words {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.word {
|
||||
display: block;
|
||||
height: 100%;
|
||||
padding-left: 6px;
|
||||
color: #ffca29;
|
||||
animation: spin_4991 4s infinite;
|
||||
}
|
||||
|
||||
@keyframes spin_4991 {
|
||||
10% {
|
||||
-webkit-transform: translateY(-105%);
|
||||
transform: translateY(-105%);
|
||||
}
|
||||
|
||||
25% {
|
||||
-webkit-transform: translateY(-100%);
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
35% {
|
||||
-webkit-transform: translateY(-205%);
|
||||
transform: translateY(-205%);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translateY(-200%);
|
||||
transform: translateY(-200%);
|
||||
}
|
||||
|
||||
60% {
|
||||
-webkit-transform: translateY(-305%);
|
||||
transform: translateY(-305%);
|
||||
}
|
||||
|
||||
75% {
|
||||
-webkit-transform: translateY(-300%);
|
||||
transform: translateY(-300%);
|
||||
}
|
||||
|
||||
85% {
|
||||
-webkit-transform: translateY(-405%);
|
||||
transform: translateY(-405%);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translateY(-400%);
|
||||
transform: translateY(-400%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- <img style="margin-bottom: 8px;" width="64px" src="/vite.svg" /> -->
|
||||
<div class="loader">
|
||||
<!-- <img src="/vite.svg"> -->
|
||||
<div> loading ...</div>
|
||||
<!-- <div class="words">
|
||||
<span class="word">buttons</span>
|
||||
<span class="word">forms</span>
|
||||
<span class="word">switches</span>
|
||||
<span class="word">cards</span>
|
||||
<span class="word">buttons</span>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="" style="font-size: 15px; color: rgba(249, 244, 244, 0.55)">首次加载可能较慢,请耐心等待...</div>
|
||||
<!-- <div class="loading-text">正在加载资源,请稍候...</div> -->
|
||||
<!-- 呼吸圆环加载组件 -->
|
||||
<div class="breath-ring"></div>
|
||||
<!-- <div class="loading-tip">首次加载较慢,请耐心等待...</div> -->
|
||||
</div>
|
||||
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<script>
|
||||
if (!global) {
|
||||
@@ -120,11 +146,4 @@
|
||||
</script>
|
||||
</body>
|
||||
|
||||
<!-- <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/font-awesome/5.15.4/css/all.min.css"> -->
|
||||
<style>
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
</html>
|
||||
@@ -54,9 +54,9 @@
|
||||
{{ item.uperName }}
|
||||
<!-- 非关注小标记 -->
|
||||
<span v-if="item.isNoFollowed" class="no-followed-badge">非关注</span>
|
||||
<!-- 删除按钮(仅非关注项显示,放在名字+非关注后面) -->
|
||||
<a-button v-if="item.isNoFollowed" type="text" class="delete-btn" @click="(e) => { e.stopPropagation(); handleDeleteItem(item); }" :disabled="item.isSaving" title="删除该非关注博主">
|
||||
<DeleteOutlined />
|
||||
<!-- 删除按钮(仅非关注项显示,放在名字+非关注后面)v-if="item.isNoFollowed" -->
|
||||
<a-button type="text" class="delete-btn" @click="(e) => { e.stopPropagation(); handleDeleteItem(item); }" :disabled="item.isSaving" title="删除该非关注博主">
|
||||
<close-outlined />
|
||||
</a-button>
|
||||
</div>
|
||||
<!-- 签名多行显示:高度控制 + 溢出截断 + Tooltip气泡 -->
|
||||
@@ -842,7 +842,7 @@ const goDouyinUp = (item) => {
|
||||
height: 24px !important;
|
||||
width: 24px !important;
|
||||
color: #ef4444 !important;
|
||||
border-radius: 4px !important;
|
||||
border-radius: 50% !important;
|
||||
transition: all 0.2s ease;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -185,23 +185,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增:视频播放弹窗(移动端适配) -->
|
||||
<!-- 新增:视频播放弹窗(全屏适配+暂停显示删除按钮) -->
|
||||
<div v-if="showVideoPlayer" class="video-modal-mask" @click="closeVideoPlayer">
|
||||
<div class="video-modal-content" @click.stop>
|
||||
<div class="video-modal-header">
|
||||
<h3 class="video-title-text"></h3>
|
||||
<button class="video-close-btn" @click="closeVideoPlayer">
|
||||
<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>
|
||||
<!-- 移除原有标题栏,将关闭按钮直接放在内容容器内,实现悬浮 -->
|
||||
<button class="video-close-btn floating-close-btn" @click="closeVideoPlayer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ffffff" 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 class="video-modal-body">
|
||||
<!-- 加载中状态 -->
|
||||
<div v-if="videoLoading" class="video-loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>加载视频中...</p>
|
||||
</div>
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="videoError" class="video-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#f44336" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
@@ -211,15 +212,24 @@
|
||||
<p>视频加载失败</p>
|
||||
<button class="retry-btn" @click="loadVideo(currentVideo)">重试</button>
|
||||
</div>
|
||||
<video v-else class="video-player" controls autoplay playsinline :src="videoPlayUrl" @error="handleVideoError">
|
||||
您的浏览器不支持HTML5视频播放
|
||||
</video>
|
||||
<!-- 视频播放区域:添加ref用于获取视频实例,绑定暂停/播放事件 -->
|
||||
<div v-else class="video-player-wrapper">
|
||||
<video ref="videoPlayerRef" class="video-player" controls autoplay playsinline :src="videoPlayUrl" @error="handleVideoError" @pause="onVideoPause" @play="onVideoPlay">
|
||||
您的浏览器不支持HTML5视频播放
|
||||
</video>
|
||||
<!-- 悬浮透明删除X按钮:仅暂停时显示 -->
|
||||
<button class="video-delete-btn" v-show="isVideoPaused" @click.stop="deleteCurrentVideo" title="删除该视频">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useApiStore } from '@/store';
|
||||
@@ -286,6 +296,9 @@ const currentVideo = ref<TopVideoItem | null>(null);
|
||||
const videoPlayUrl = ref<string>('');
|
||||
const videoLoading = ref<boolean>(false);
|
||||
const videoError = ref<boolean>(false);
|
||||
// 新增:视频实例引用和暂停状态
|
||||
const videoPlayerRef = ref<HTMLVideoElement | null>(null);
|
||||
const isVideoPaused = ref<boolean>(false); // 控制删除按钮显示
|
||||
|
||||
// 过滤后的日志列表
|
||||
const filteredLogs = computed(() => {
|
||||
@@ -453,7 +466,12 @@ const openVideoPlayer = (video: TopVideoItem) => {
|
||||
|
||||
currentVideo.value = video;
|
||||
showVideoPlayer.value = true;
|
||||
loadVideo(video);
|
||||
loadVideo(video).then(() => {
|
||||
// 视频加载完成后触发播放,避免浏览器拦截
|
||||
if (videoPlayerRef.value) {
|
||||
videoPlayerRef.value.play().catch((err) => console.log('自动播放被拦截:', err));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 新增:加载视频播放地址
|
||||
@@ -479,6 +497,7 @@ const closeVideoPlayer = () => {
|
||||
videoPlayUrl.value = '';
|
||||
videoLoading.value = false;
|
||||
videoError.value = false;
|
||||
isVideoPaused.value = false; // 重置暂停状态
|
||||
};
|
||||
|
||||
// 新增:处理视频播放错误
|
||||
@@ -486,6 +505,23 @@ const handleVideoError = () => {
|
||||
videoError.value = true;
|
||||
console.error('视频播放出错');
|
||||
};
|
||||
|
||||
// 新增:视频暂停回调
|
||||
const onVideoPause = () => {
|
||||
isVideoPaused.value = true;
|
||||
};
|
||||
|
||||
// 新增:视频播放回调
|
||||
const onVideoPlay = () => {
|
||||
isVideoPaused.value = false;
|
||||
};
|
||||
|
||||
const deleteCurrentVideo = async () => {
|
||||
// 关闭播放弹窗,刷新视频列表
|
||||
closeVideoPlayer();
|
||||
TopVideo(); // 重新加载最新视频列表
|
||||
loadDashboardData(); // 刷新仪表盘统计数据
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -1003,68 +1039,58 @@ const handleVideoError = () => {
|
||||
.copy-btn:hover:not(:disabled) {
|
||||
background-color: #43a047;
|
||||
}
|
||||
|
||||
/* 新增:视频播放弹窗样式(移动端适配) */
|
||||
/* 视频播放弹窗样式(纯全屏+悬浮右上角关闭按钮) */
|
||||
.video-modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
background-color: #000; /* 纯黑背景更贴合全屏播放 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
padding: 0px;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.video-modal-content {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
width: 100vw; /* 视口宽度100% */
|
||||
height: 100vh; /* 视口高度100% */
|
||||
max-width: none; /* 移除原有最大宽度限制 */
|
||||
max-height: none; /* 移除原有最大高度限制 */
|
||||
background-color: #111;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||
max-height: 80vh;
|
||||
border-radius: 0; /* 全屏移除圆角 */
|
||||
box-shadow: none; /* 全屏无需阴影 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative; /* 为悬浮关闭按钮提供定位上下文 */
|
||||
}
|
||||
.video-modal-header {
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #222;
|
||||
}
|
||||
.video-title-text {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 80%;
|
||||
}
|
||||
.video-close-btn {
|
||||
background: transparent;
|
||||
/* 悬浮右上角关闭按钮样式 */
|
||||
.floating-close-btn {
|
||||
position: absolute;
|
||||
top: 20px; /* 右上角间距,可调整 */
|
||||
right: 20px; /* 右上角间距,可调整 */
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 2010; /* 确保在视频上方 */
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
.video-close-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
.floating-close-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.video-modal-body {
|
||||
padding: 0;
|
||||
@@ -1074,13 +1100,44 @@ const handleVideoError = () => {
|
||||
justify-content: center;
|
||||
background-color: #000;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%; /* 完全填充父容器,无高度损耗 */
|
||||
}
|
||||
/* 新增:视频播放器容器(用于定位删除按钮) */
|
||||
.video-player-wrapper {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
.video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
max-height: 60vh;
|
||||
object-fit: contain;
|
||||
object-fit: contain; /* 保持视频比例,全屏填充无黑边(也可改为 cover 强制填充,可能裁剪视频) */
|
||||
}
|
||||
/* 悬浮透明删除X按钮样式 */
|
||||
.video-delete-btn {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
right: 20px;
|
||||
transform: translateY(-50%);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 2010;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
.video-delete-btn:hover,
|
||||
.video-delete-btn:active {
|
||||
background-color: rgba(244, 67, 54, 0.5);
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
}
|
||||
/* 视频加载状态 */
|
||||
.video-loading {
|
||||
@@ -1090,6 +1147,8 @@ const handleVideoError = () => {
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
padding: 40px 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
@@ -1114,6 +1173,8 @@ const handleVideoError = () => {
|
||||
color: #fff;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.video-error p {
|
||||
margin: 16px 0 24px;
|
||||
@@ -1277,6 +1338,15 @@ html.dark-mode .close-btn:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 夜间模式 - 视频弹窗样式适配 */
|
||||
html.dark-mode .video-delete-btn {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
html.dark-mode .video-delete-btn:hover,
|
||||
html.dark-mode .video-delete-btn:active {
|
||||
background-color: rgba(244, 67, 54, 0.6);
|
||||
}
|
||||
|
||||
.ant-message {
|
||||
z-index: 10000 !important;
|
||||
}
|
||||
|
||||
+128
-162
@@ -25,7 +25,7 @@
|
||||
<a-switch v-model:checked="formState.OnlySyncNew" />
|
||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
||||
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
||||
<span>开启后,仅同步最近收藏的20条,以及未来新收藏的,不会去同步之前的视频,默认开启, 避免突然大量下载,导致风控</span>
|
||||
<span>开启后,仅同步最近的20条,以及未来新加入收藏、喜欢、关注的视频,不会去同步之前的视频,默认开启, 避免突然大量下载,导致风控</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</div>
|
||||
@@ -113,32 +113,44 @@
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="formState.DownImageVideo" has-feedback label="默认音频" name="AudioFile" :wrapper-col="{ span: 20 }">
|
||||
<!-- 音频上传与播放器容器 -->
|
||||
<div class="audio-upload-player-wrapper" style="display: flex; align-items: center; gap: 16px;">
|
||||
<a-upload :before-upload="beforeUpload" :custom-request="customUpload" :show-upload-list="false" accept=".mp3,.wav">
|
||||
<a-button type="default">
|
||||
<UploadOutlined /> 选择文件
|
||||
</a-button>
|
||||
</a-upload>
|
||||
|
||||
<!-- 新增:启用原生完整控件(controls属性),自带可拖拽进度条 -->
|
||||
<div class="audio-player" v-if="audioUrl" style="flex: 1; max-width: 500px;">
|
||||
<audio ref="audioInstance" :src="audioUrl" controls controlsList="nodownload" @ended="() => isPlaying = false" @pause="() => isPlaying = false" @play="() => isPlaying = true" class="native-audio-player">
|
||||
您的浏览器不支持HTML5音频播放,请升级至现代浏览器。
|
||||
</audio>
|
||||
</div>
|
||||
<a-form-item has-feedback label="默认音频" name="AudioFile" :wrapper-col="{ span: 20 }">
|
||||
<!-- 仅保留检测按钮 -->
|
||||
<div class="audio-check-wrapper" style="display: flex; align-items: center; gap: 16px;">
|
||||
<a-button type="primary" @click="checkAudioList">
|
||||
<play-circle-outlined /> 音频列表
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 提示文本调整 -->
|
||||
<div class="flex items-start mt-1 text-sm text-gray-500" style="color:red">
|
||||
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
||||
<span>当下载图文视频时,音频因版权原因无法下载时,将用该音频文件作为合成视频的音频</span>
|
||||
</div>
|
||||
<div class="flex items-start mt-1 text-sm text-gray-500">
|
||||
<InfoCircleOutlined class="text-blue-400 mr-1 mt-0.5" />
|
||||
<span>支持格式:MP3、WAV、AAC、FLAC、OGG、M4A、WMA,单个文件最大20MB</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-modal v-model:visible="audioListModalVisible" title="发现音频" width="666px" destroyOnClose @ok="closeAudioListModal" @cancel="closeAudioListModal">
|
||||
<div class="audio-list-container" style="max-height: 500px; overflow-y: auto; padding: 10px 0;">
|
||||
<!-- 空列表提示 -->
|
||||
<div v-if="audioList.length === 0" style="text-align: center; padding: 40px; color: #999;">
|
||||
暂无可用音频文件
|
||||
</div>
|
||||
|
||||
<!-- 音频列表(嵌入原生audio播放器) -->
|
||||
<a-list v-else bordered :data-source="audioList" item-layout="horizontal">
|
||||
<template #renderItem="{item}">
|
||||
<a-list-item>
|
||||
<a-list-item-meta :title="item.filename " />
|
||||
<template #actions>
|
||||
<!-- 替换原播放按钮,嵌入原生HTML音频播放器 -->
|
||||
<audio class="native-audio-player" controls :src="'/api/config/getmp3?name='+item.filename" preload="none" @play="handleAudioPlay($event.target)">
|
||||
您的浏览器不支持HTML5音频播放器,请升级浏览器后重试。
|
||||
</audio>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
@@ -226,7 +238,7 @@
|
||||
<input ref="importFileInput" type="file" accept=".json" class="import-file-input" @change="handleImportFile">
|
||||
</div>
|
||||
<div class="top-right-float-btn-container">
|
||||
<a-tooltip title="重新对同步好的视频文件进行刮削" placement="bottom">
|
||||
<a-tooltip title="重置所有刮削" placement="bottom">
|
||||
<a-button class="top-right-float-btn" type="primary" shape="circle" @click="renfo">
|
||||
<bulb-outlined />
|
||||
</a-button>
|
||||
@@ -276,9 +288,6 @@ const downImgVideo = ref(true);
|
||||
const floatMenuVisible = ref(false);
|
||||
const importFileInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
// 新增:文件上传相关状态(已删除 uploadFileList,仅保留 isUploading 可选)
|
||||
const isUploading = ref(false);
|
||||
|
||||
//开启或关闭合成视频
|
||||
const downImageVideoHandler = () => {
|
||||
if (formState.DownImageVideo) {
|
||||
@@ -405,7 +414,11 @@ const getConfig = () => {
|
||||
|
||||
tagData.value = JSON.parse(res.data.priorityLevel || '[]');
|
||||
} else {
|
||||
message.error(res.message || '获取配置失败', 8);
|
||||
if (res.message.indexOf('401') == -1) {
|
||||
message.error(res.message || '获取配置失败', 8);
|
||||
} else {
|
||||
console.error('获取配置失败:', res);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -443,135 +456,65 @@ const updateTagSort = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// ========== 文件上传相关方法(已修改:移除进度条逻辑) ==========
|
||||
// 音频列表相关状态
|
||||
const audioListModalVisible = ref(false); // 弹窗显隐
|
||||
const audioList = ref<any[]>([]); // 存储接口返回的音频列表
|
||||
const currentPlayingAudio = ref<HTMLAudioElement | null>(null); // 核心:存储当前正在播放的音频
|
||||
|
||||
/**
|
||||
* 上传前校验
|
||||
* 音频播放互斥处理(核心:同一时间只播放一个音频)
|
||||
* @param target 事件目标元素(EventTarget 类型)
|
||||
*/
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
// 1. 校验文件大小(10MB)
|
||||
const isLt20M = file.size / 1024 / 1024 < 20;
|
||||
if (!isLt20M) {
|
||||
message.error('文件大小不能超过20MB!');
|
||||
return false;
|
||||
const handleAudioPlay = (target: EventTarget | null) => {
|
||||
// 1. 校验元素是否存在,且是 HTMLAudioElement 类型
|
||||
if (!target || !(target instanceof HTMLAudioElement)) {
|
||||
return; // 非音频元素,直接返回,避免报错
|
||||
}
|
||||
|
||||
// 2. 校验文件类型
|
||||
const acceptTypes = ['.mp3', '.wav', '.aac', '.flac', '.ogg', '.m4a', '.wma'];
|
||||
const fileExt = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
if (!acceptTypes.includes(fileExt || '')) {
|
||||
message.error('仅支持MP3、WAV、AAC、FLAC、OGG、M4A、WMA格式的音频文件!');
|
||||
return false;
|
||||
// 2. 此时 target 已被确认是 HTMLAudioElement 类型,可安全操作
|
||||
const audioEl = target;
|
||||
// 3. 原有互斥逻辑
|
||||
if (currentPlayingAudio.value && currentPlayingAudio.value !== audioEl) {
|
||||
currentPlayingAudio.value.pause();
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
// 1. 新增音频播放相关状态(放在现有状态定义区域,如 isUploading 下方)
|
||||
const audioUrl = ref('/api/config/defaudio'); // 上传成功后的音频文件URL
|
||||
const isPlaying = ref(false); // 音频是否正在播放
|
||||
const audioInstance = ref<HTMLAudioElement | null>(null); // 音频播放器实例
|
||||
|
||||
// 2. 改造原有 customUpload 方法,保存上传成功后的音频URL
|
||||
const customUpload: UploadProps['customRequest'] = (options) => {
|
||||
const { file, onSuccess, onError } = options;
|
||||
isUploading.value = true;
|
||||
|
||||
// 构造FormData
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
useApiStore()
|
||||
.apiUploadAudio(formData)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
if (res.code === 0) {
|
||||
// message.success('音频文件上传成功!');
|
||||
audioUrl.value = `/api/config/defaudio?t=${Date.now()}`;
|
||||
onSuccess(res);
|
||||
} else {
|
||||
message.error(res.message || '文件上传失败!');
|
||||
onError(new Error(res.message || '上传失败'), file);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('文件上传失败:', error);
|
||||
message.error('文件上传失败,请稍后重试!');
|
||||
onError(error, file);
|
||||
})
|
||||
.finally(() => {
|
||||
isUploading.value = false;
|
||||
});
|
||||
currentPlayingAudio.value = audioEl;
|
||||
};
|
||||
|
||||
// 新增:封装 load() 为 Promise (可复用)
|
||||
const audioLoadPromise = (audio: HTMLAudioElement): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 加载成功回调
|
||||
const onLoadSuccess = () => {
|
||||
audio.removeEventListener('canplaythrough', onLoadSuccess);
|
||||
audio.removeEventListener('error', onLoadError);
|
||||
resolve();
|
||||
};
|
||||
|
||||
// 加载失败回调
|
||||
const onLoadError = () => {
|
||||
audio.removeEventListener('canplaythrough', onLoadSuccess);
|
||||
audio.removeEventListener('error', onLoadError);
|
||||
reject(new Error('音频加载失败'));
|
||||
};
|
||||
|
||||
audio.addEventListener('canplaythrough', onLoadSuccess);
|
||||
audio.addEventListener('error', onLoadError);
|
||||
audio.load();
|
||||
});
|
||||
};
|
||||
// 修改 refreshAudioPlayer 为 async 方法
|
||||
const refreshAudioPlayer = async () => {
|
||||
if (!audioInstance.value) return;
|
||||
|
||||
/**
|
||||
* 检测音频列表:调用mp3List接口并展示弹窗
|
||||
*/
|
||||
const checkAudioList = async () => {
|
||||
try {
|
||||
// 1. 暂停播放、重置状态
|
||||
audioInstance.value.pause();
|
||||
audioInstance.value.currentTime = 0;
|
||||
isPlaying.value = false;
|
||||
|
||||
// 2. 等待加载完成(核心:解决时序冲突)
|
||||
await audioLoadPromise(audioInstance.value);
|
||||
|
||||
// 3. 加载完成后,安全调用 play()
|
||||
await audioInstance.value.play();
|
||||
isPlaying.value = true;
|
||||
// message.success('音频已自动播放');
|
||||
} catch (err) {
|
||||
if ((err as Error).message !== '音频加载失败') {
|
||||
// 区分加载失败和自动播放失败
|
||||
message.warning('自动播放失败,请手动点击播放按钮(浏览器限制)');
|
||||
} else {
|
||||
message.error('音频加载失败,无法自动播放');
|
||||
// 步骤1:打开弹窗前,先暂停上一次可能残留的播放音频
|
||||
if (currentPlayingAudio.value) {
|
||||
currentPlayingAudio.value.pause();
|
||||
currentPlayingAudio.value = null;
|
||||
}
|
||||
console.log('错误详情:', err);
|
||||
isPlaying.value = false;
|
||||
|
||||
// 步骤2:调用接口获取音频列表
|
||||
const res = await useApiStore().mp3List();
|
||||
if (res.code === 0) {
|
||||
audioList.value = res.data || [];
|
||||
audioListModalVisible.value = true;
|
||||
} else {
|
||||
message.error(res.message || '获取音频列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检测音频列表失败:', error);
|
||||
message.error('获取音频列表失败,请稍后重试');
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 关闭音频列表弹窗
|
||||
*/
|
||||
const closeAudioListModal = () => {
|
||||
audioListModalVisible.value = false;
|
||||
// 清理当前播放的音频,避免弹窗关闭后仍在播放或残留实例
|
||||
if (currentPlayingAudio.value) {
|
||||
currentPlayingAudio.value.pause();
|
||||
currentPlayingAudio.value = null;
|
||||
}
|
||||
};
|
||||
// 监听 audioUrl 变化,重置播放状态(可选,优化用户体验)
|
||||
watch(
|
||||
audioUrl,
|
||||
(newVal, oldVal) => {
|
||||
// 排除初始值(仅当 url 发生有效变更时刷新)
|
||||
if (newVal && newVal !== oldVal) {
|
||||
isPlaying.value = false;
|
||||
// 核心:调用刷新方法,强制加载新音频
|
||||
refreshAudioPlayer();
|
||||
} else if (!newVal) {
|
||||
// 若 url 为空,仅重置状态
|
||||
isPlaying.value = false;
|
||||
if (audioInstance.value) {
|
||||
audioInstance.value.currentTime = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
); // 关闭 immediate,避免初始加载时触发
|
||||
|
||||
// 组件挂载时获取配置
|
||||
onMounted(async () => {
|
||||
getConfig();
|
||||
@@ -938,29 +881,24 @@ const renfo = () => {
|
||||
:deep(.ant-upload.ant-upload-select) {
|
||||
display: inline-block;
|
||||
}
|
||||
// 音频列表弹窗样式优化
|
||||
:deep(.audio-list-container .ant-list) {
|
||||
.ant-list-item {
|
||||
padding: 12px 16px;
|
||||
&:hover {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
}
|
||||
|
||||
// 音频上传与播放器容器样式
|
||||
.audio-upload-player-wrapper {
|
||||
flex-wrap: wrap;
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
.ant-list-item-meta-title {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// 音频播放器样式优化
|
||||
:deep(.audio-player audio) {
|
||||
// border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
// 播放/清空按钮 hover 效果
|
||||
:deep(.audio-player .ant-btn-text:hover) {
|
||||
background-color: #f5f5f5;
|
||||
// 检测按钮样式
|
||||
.audio-check-wrapper {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
// 右上角悬浮按钮容器
|
||||
@@ -987,4 +925,32 @@ const renfo = () => {
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15); // 悬浮时阴影加深(可选,可删除)
|
||||
}
|
||||
}
|
||||
|
||||
// 原生音频播放器样式优化
|
||||
.native-audio-player {
|
||||
width: 200px; // 固定宽度,适配列表布局
|
||||
height: 32px; // 统一高度,与列表项对齐
|
||||
outline: none; // 移除聚焦轮廓
|
||||
border: 1px solid #d9d9d9; // 添边框,与AntD风格统一
|
||||
border-radius: 4px; // 圆角,与AntD风格统一
|
||||
background-color: #fafafa; // 浅背景色,提升质感
|
||||
|
||||
// 优化播放器内部控件样式(部分浏览器支持)
|
||||
&::-webkit-media-controls {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
// 悬浮效果
|
||||
&:hover {
|
||||
border-color: #1890ff; // 悬浮时边框变主色,提升交互感
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); // 轻微发光效果
|
||||
}
|
||||
}
|
||||
|
||||
// 音频列表项适配,保证播放器与内容对齐
|
||||
:deep(.ant-list-item-actions) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -44,18 +44,24 @@
|
||||
<a-space size="middle" class="button-group">
|
||||
<a-button success @click="handleBatchShare" class="delete-button" v-if="isBatchMode" :disabled="selectedRowKeys.length === 0 || isSyncing">
|
||||
<ShareAltOutlined />
|
||||
分享选中
|
||||
批量分享
|
||||
</a-button>
|
||||
<a-button type="danger" @click="handleBatchDelete" class="delete-button" v-if="isBatchMode" :disabled="selectedRowKeys.length === 0 || isSyncing">
|
||||
<a-button danger @click="handleBatchSync" class="delete-button" v-if="isBatchMode" :disabled="selectedRowKeys.length === 0 || isSyncing">
|
||||
<SyncOutlined />
|
||||
重新同步
|
||||
重新下载
|
||||
</a-button>
|
||||
<a-button danger @click="handleBatchDelete" class="delete-button" v-if="isBatchMode" :disabled="selectedRowKeys.length === 0 || isSyncing">
|
||||
<close-outlined />
|
||||
批量删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
<!-- 按钮代码 -->
|
||||
<a-form-item class="form-item delete-btn-2-wrapper">
|
||||
<a-button danger @click="handShowDeleteVideos" class="delete-button-2">
|
||||
<ClearOutlined /> <!-- 注意首字母大写,Antd图标命名规范 -->
|
||||
<a-button type="primary" danger @click="handShowDeleteVideos" class="delete-button-2">
|
||||
<!-- <ClearOutlined /> -->
|
||||
<!-- 注意首字母大写,Antd图标命名规范 -->
|
||||
<delete-outlined />
|
||||
已删除
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
@@ -79,9 +85,9 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<a-button type="text" size="small" class="copy-delete-video-btn" @click="(e) => copyVideoPath(item.videoSavePath)">
|
||||
<!-- <a-button type="text" size="small" class="copy-delete-video-btn" @click="(e) => copyVideoPath(item.videoSavePath)">
|
||||
<CopyOutlined /> 复制
|
||||
</a-button>
|
||||
</a-button> -->
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
@@ -639,9 +645,9 @@ watch(
|
||||
|
||||
// -------------------------- 批量操作和操作列事件 --------------------------
|
||||
/** 批量删除事件 */
|
||||
const handleBatchDelete = () => {
|
||||
const handleBatchSync = () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.warning('请先选择要删除的视频');
|
||||
message.warning('请先选择要重新下载的视频');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -657,6 +663,24 @@ const handleBatchDelete = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.warning('请先选择要彻底删除的视频');
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: '确认删除这些下载的视频吗',
|
||||
content: `您确定要彻底下删除选中的 ${selectedRowKeys.value.length} 条视频数据吗?`,
|
||||
okText: '确认彻底删除',
|
||||
cancelText: '取消',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
deleteBatch({ ids: selectedRowKeys.value });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteVideoShow = ref(false);
|
||||
const handShowDeleteVideos = () => {
|
||||
deleteVideoShow.value = true;
|
||||
@@ -704,6 +728,35 @@ const reDownload = (param: object) => {
|
||||
}
|
||||
};
|
||||
|
||||
const deleteBatch = (param: object) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
console.log('执行批量删除,选中ID:', selectedRowKeys.value);
|
||||
|
||||
useApiStore()
|
||||
.BathRealDelete(param)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res.code === 0) {
|
||||
message.success('删除成功,以后都不会下载了哦,你自己选的');
|
||||
// 刷新数据并清空选中状态
|
||||
GetRecords();
|
||||
selectedRowKeys.value = [];
|
||||
} else {
|
||||
message.warning(res.message || '获取数据失败');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
loading.value = false;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error);
|
||||
message.error('删除失败,请稍后重试');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 重新下载事件 */
|
||||
const handleReDownload = (record: DataItem) => {
|
||||
if (!record.id) {
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
<!-- 作者统计 -->
|
||||
<div v-if="currentTab === 'author'" key="author-view" class="stats-content">
|
||||
<div class="authors-grid">
|
||||
<div class="author-card" v-for="(author, index) in authors" :key="index">
|
||||
<div class="author-card" v-for="(author, index) in authors" :key="index" @dblclick="handleDeleteItem(author)">
|
||||
<!-- 新增横向容器:包裹头像和作者信息 -->
|
||||
<div class="author-info-row">
|
||||
<div class="author-avatar">
|
||||
@@ -209,7 +209,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useApiStore } from '@/store';
|
||||
|
||||
import { message, Spin, Empty, Tooltip, Modal, Form, FormInstance, Popconfirm } from 'ant-design-vue';
|
||||
// 类型接口
|
||||
interface Author {
|
||||
name: string;
|
||||
@@ -313,6 +313,37 @@ const getRandomElements = (arr: any[], n: number) => {
|
||||
if (n >= arr.length) return [...arr];
|
||||
return [...arr].sort(() => Math.random() - 0.5).slice(0, n);
|
||||
};
|
||||
|
||||
const handleDeleteItem = (item: any) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除博主「${item.name}」所有视频吗?删除后将无法恢复。`,
|
||||
okText: '确认删除',
|
||||
cancelText: '取消',
|
||||
okType: 'danger',
|
||||
maskClosable: false,
|
||||
onOk: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
useApiStore()
|
||||
.DeleteByAuthor(item.uperId)
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
message.success('根据视频数量,需要时常不确定,可以稍后去日志查看...');
|
||||
resolve(true);
|
||||
} else {
|
||||
message.error('删除博主视频失败' + (res.message || '未知错误'));
|
||||
reject(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('删除博主视频异常', err);
|
||||
message.error('删除博主视频异常' + err);
|
||||
reject(false);
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
/* 基础样式 */
|
||||
|
||||
+46
-10
@@ -31,18 +31,25 @@ import { Response } from '@/types';
|
||||
export const useApiStore = defineStore('coreapi', () => {
|
||||
|
||||
|
||||
|
||||
//获取配置
|
||||
async function apiGetConfig() {
|
||||
return http
|
||||
.request<any, Response<any>>('/api/config/GetConfig', 'GET')
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.finally(() => {
|
||||
return http.request<any, Response<any>>('/api/config/GetConfig', 'get').then(r => {
|
||||
return r;
|
||||
}).finally(() => {
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// //获取配置
|
||||
// async function apiGetConfig() {
|
||||
// return http
|
||||
// .request<any, Response<any>>('/api/config/GetConfig', 'GET')
|
||||
// .then((res) => {
|
||||
// return res;
|
||||
// })
|
||||
// .finally(() => {
|
||||
|
||||
// });
|
||||
// }
|
||||
//修改配置
|
||||
async function apiUpdateConfig(request: object) {
|
||||
return http
|
||||
@@ -201,6 +208,14 @@ export const useApiStore = defineStore('coreapi', () => {
|
||||
|
||||
});
|
||||
}
|
||||
//批量删除
|
||||
async function BathRealDelete(param: object) {
|
||||
return http.request<any, Response<any>>('/api/video/vdelete/batch', 'post_json', param).then(r => {
|
||||
return r;
|
||||
}).finally(() => {
|
||||
|
||||
});
|
||||
}
|
||||
//删除
|
||||
async function DeleteVideo(param: string) {
|
||||
return http.request<any, Response<any>>('/api/video/vdelete/' + param, 'get').then(r => {
|
||||
@@ -209,7 +224,7 @@ export const useApiStore = defineStore('coreapi', () => {
|
||||
|
||||
});
|
||||
}
|
||||
//删除
|
||||
//查询已删除
|
||||
async function GetDeleteViedos() {
|
||||
return http.request<any, Response<any>>('/api/video/vdelete/get', 'get').then(r => {
|
||||
return r;
|
||||
@@ -217,6 +232,14 @@ export const useApiStore = defineStore('coreapi', () => {
|
||||
|
||||
});
|
||||
}
|
||||
//删除博主全部视频
|
||||
async function DeleteByAuthor(param: string) {
|
||||
return http.request<any, Response<any>>('/api/video/vdelete/byauthor/' + param, 'get').then(r => {
|
||||
return r;
|
||||
}).finally(() => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//检查版本
|
||||
@@ -227,6 +250,16 @@ export const useApiStore = defineStore('coreapi', () => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
async function mp3List() {
|
||||
return http.request<any, Response<any>>('/api/config/mp3List', 'get').then(r => {
|
||||
return r;
|
||||
}).finally(() => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//快速停止或启动cookie配置
|
||||
async function SwitchCookieStatus(param: object) {
|
||||
return http.request<any, Response<any>>('/api/config/switch', 'post_json', param).then(r => {
|
||||
@@ -331,6 +364,9 @@ export const useApiStore = defineStore('coreapi', () => {
|
||||
}
|
||||
|
||||
return {
|
||||
mp3List,
|
||||
BathRealDelete,
|
||||
DeleteByAuthor,
|
||||
Renfo,
|
||||
apiUploadAudio,
|
||||
GetAppPort,
|
||||
|
||||
+14
-16
@@ -48,23 +48,21 @@ http.interceptors.response.use(
|
||||
accountStore.setLogged(false);
|
||||
message.warning('登录状态已过期,请重新登录');
|
||||
|
||||
setTimeout(() => {
|
||||
const redirectPath = router.currentRoute.value.fullPath;
|
||||
if (redirectPath !== '/login') {
|
||||
router.push({
|
||||
path: '/login',
|
||||
query: { redirect: redirectPath }
|
||||
}).then(() => {
|
||||
console.log('跳转登录页成功');
|
||||
}).catch((err) => {
|
||||
console.error('跳转登录页失败:', err);
|
||||
}).finally(() => {
|
||||
isRedirecting = false;
|
||||
});
|
||||
} else {
|
||||
const redirectPath = router.currentRoute.value.fullPath;
|
||||
if (redirectPath !== '/login') {
|
||||
router.push({
|
||||
path: '/login',
|
||||
query: { redirect: redirectPath }
|
||||
}).then(() => {
|
||||
console.log('跳转登录页成功');
|
||||
}).catch((err) => {
|
||||
console.error('跳转登录页失败:', err);
|
||||
}).finally(() => {
|
||||
isRedirecting = false;
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
} else {
|
||||
isRedirecting = false;
|
||||
}
|
||||
}
|
||||
// 新增结束
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, Method as _Method, AxiosResponse } from 'axios'; // 移除 AxiosProgressEvent
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, Method as _Method, AxiosResponse } from 'axios';
|
||||
import qs from 'qs';
|
||||
import Cookie from 'js-cookie';
|
||||
|
||||
@@ -13,8 +13,8 @@ declare interface _AxiosExtend {
|
||||
request<T = any, R = AxiosResponse<T>>(
|
||||
url: string,
|
||||
method: Method,
|
||||
params?: Record<string | number, any> | FormData, // 支持 FormData 类型
|
||||
config?: AxiosRequestConfig & { onUploadProgress?: (progressEvent: ProgressEvent) => void } // 改用原生 ProgressEvent
|
||||
params?: Record<string | number, any> | FormData,
|
||||
config?: AxiosRequestConfig & { onUploadProgress?: (progressEvent: ProgressEvent) => void }
|
||||
): Promise<R>;
|
||||
/**
|
||||
* 设置token
|
||||
@@ -40,7 +40,6 @@ declare interface _AxiosExtend {
|
||||
|
||||
export interface AxiosHttp extends Omit<AxiosInstance, 'request'>, _AxiosExtend { }
|
||||
|
||||
// 新增 post_form / POST_FORM 类型
|
||||
export type Method = _Method | 'POST_JSON' | 'post_json' | 'PUT_JSON' | 'put_json' | 'POST_FORM' | 'post_form';
|
||||
|
||||
/**
|
||||
@@ -83,22 +82,10 @@ function toUrlencoded(params?: Record<string | number, any>) {
|
||||
function createAxiosHttp(config: AxiosRequestConfig): AxiosHttp {
|
||||
const _axios = axios.create(config);
|
||||
|
||||
// 添加响应拦截器处理401状态
|
||||
// 移除 401 处理,仅保留基础的响应拦截器(可选,也可直接删除)
|
||||
_axios.interceptors.response.use(
|
||||
// 成功响应直接返回
|
||||
response => response,
|
||||
// 错误响应处理
|
||||
error => {
|
||||
// 检查是否是401未授权错误
|
||||
if (error.response && error.response.status === 401) {
|
||||
// 调用removeAuthorization方法清除token
|
||||
http.removeAuthorization();
|
||||
|
||||
// 这里可以添加额外的处理,比如跳转到登录页
|
||||
// 示例: window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
error => Promise.reject(error)
|
||||
);
|
||||
|
||||
const http: AxiosHttp = {
|
||||
@@ -107,13 +94,12 @@ function createAxiosHttp(config: AxiosRequestConfig): AxiosHttp {
|
||||
url: string,
|
||||
method: Method,
|
||||
params?: Record<string | number, any> | FormData,
|
||||
config?: AxiosRequestConfig & { onUploadProgress?: (progressEvent: ProgressEvent) => void } // 改用原生 ProgressEvent
|
||||
config?: AxiosRequestConfig & { onUploadProgress?: (progressEvent: ProgressEvent) => void }
|
||||
): Promise<R> {
|
||||
const _method = method.toUpperCase();
|
||||
// 处理上传进度配置
|
||||
const requestConfig: AxiosRequestConfig = {
|
||||
...config,
|
||||
onUploadProgress: config?.onUploadProgress, // 透传上传进度回调
|
||||
onUploadProgress: config?.onUploadProgress,
|
||||
};
|
||||
|
||||
switch (_method) {
|
||||
@@ -132,11 +118,9 @@ function createAxiosHttp(config: AxiosRequestConfig): AxiosHttp {
|
||||
...requestConfig,
|
||||
headers: { 'Content-Type': 'application/json', ...requestConfig.headers },
|
||||
});
|
||||
// 新增:POST_FORM 类型(适配文件上传的 FormData)
|
||||
case 'POST_FORM':
|
||||
return _axios.post(url, params, {
|
||||
...requestConfig,
|
||||
// FormData 不需要手动设置 Content-Type,axios 会自动处理为 multipart/form-data
|
||||
headers: { ...requestConfig.headers },
|
||||
});
|
||||
case 'PUT':
|
||||
|
||||
Reference in New Issue
Block a user