mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-08 21:02:55 +08:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2d6ca75be | ||
|
|
a4a9f9e7c5 | ||
|
|
4d5d1094ed | ||
|
|
9f8eaa5722 | ||
|
|
79e07d2b3d | ||
|
|
822d457bff | ||
|
|
8e391af0b4 | ||
|
|
b522b4d355 | ||
|
|
9c5ef8f5b4 | ||
|
|
4dc1ad35d2 | ||
|
|
9b405d9e59 | ||
|
|
2351ec7b85 | ||
|
|
6fc3228334 |
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@@ -34,6 +34,13 @@ jobs:
|
||||
yarn build
|
||||
zip -r dist.zip dist
|
||||
|
||||
- name: Delete Release
|
||||
uses: dev-drprasad/delete-tag-and-release@v1.0
|
||||
with:
|
||||
tag_name: ${{ env.frontend_version }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
delete_release: true
|
||||
|
||||
- name: Generate Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "1.0.10",
|
||||
"version": "1.1.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
@@ -59,6 +59,7 @@
|
||||
"@iconify/vue": "4.1.1",
|
||||
"@intlify/unplugin-vue-i18n": "^0.10.0",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/node": "^20.1.4",
|
||||
"@types/webfontloader": "^1.6.34",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.5",
|
||||
|
||||
@@ -88,9 +88,9 @@ export function formatSeconds(seconds: number) {
|
||||
}
|
||||
|
||||
// YYYY-MM-DD 转化为Date
|
||||
export function parseDate(dateString: string): Date {
|
||||
export function parseDate(dateString: string): Date | null {
|
||||
if (!dateString)
|
||||
return new Date()
|
||||
return null
|
||||
const [year, month, day] = dateString.split('-').map(Number)
|
||||
|
||||
return new Date(year, month - 1, day)
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useTheme } from 'vuetify'
|
||||
import miscpose from '@images/pages/pose-fs-9.png'
|
||||
import miscMaskDark from '@images/pages/misc-mask-dark.png'
|
||||
import miscMaskLight from '@images/pages/misc-mask-light.png'
|
||||
import tree from '@images/pages/tree.png'
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const vuetifyTheme = useTheme()
|
||||
|
||||
const authThemeMask = computed(() => {
|
||||
return vuetifyTheme.global.name.value === 'light' ? miscMaskLight : miscMaskDark
|
||||
})
|
||||
|
||||
interface Props {
|
||||
errorCode?: string
|
||||
errorTitle?: string
|
||||
@@ -21,7 +11,7 @@ interface Props {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="misc-wrapper">
|
||||
<div class="flex flex-col">
|
||||
<ErrorHeader
|
||||
:error-code="props.errorCode"
|
||||
:error-title="props.errorTitle"
|
||||
@@ -29,7 +19,7 @@ interface Props {
|
||||
/>
|
||||
|
||||
<!-- 👉 Image -->
|
||||
<div class="misc-avatar text-center">
|
||||
<div class="text-center">
|
||||
<VImg
|
||||
:src="miscpose"
|
||||
class="mx-auto pt-10"
|
||||
@@ -38,40 +28,8 @@ interface Props {
|
||||
/>
|
||||
<slot name="button" />
|
||||
</div>
|
||||
|
||||
<!-- 👉 Footer -->
|
||||
<VImg
|
||||
:src="tree"
|
||||
class="misc-footer-tree d-none d-lg-block"
|
||||
/>
|
||||
|
||||
<VImg
|
||||
:src="authThemeMask"
|
||||
class="misc-footer-img d-none d-md-block"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@configured-variables" as variables;
|
||||
@use '@core/scss/pages/misc.scss';
|
||||
|
||||
.misc-wrapper {
|
||||
position: relative;
|
||||
|
||||
.misc-footer-tree {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
inline-size: 15.625rem;
|
||||
inset-block-end: 3.5rem;
|
||||
inset-inline-start: 0.375rem;
|
||||
left: variables.$layout-vertical-nav-width;
|
||||
}
|
||||
|
||||
.misc-footer-img {
|
||||
position: fixed;
|
||||
inline-size: 100%;
|
||||
inset-block-end: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,7 @@ import axios from 'axios'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import { formatBytes } from '@core/utils/formatters'
|
||||
import type { EndPoints, FileItem } from '@/api/types'
|
||||
import type { Context, EndPoints, FileItem } from '@/api/types'
|
||||
import store from '@/store'
|
||||
import api from '@/api'
|
||||
|
||||
@@ -84,6 +84,9 @@ const transferForm = reactive({
|
||||
|
||||
})
|
||||
|
||||
// 识别结果
|
||||
const nameTestResult = ref<Context>()
|
||||
|
||||
// 生成1到50季的下拉框选项
|
||||
const seasonItems = ref(
|
||||
Array.from({ length: 51 }, (_, i) => i).map(item => ({
|
||||
@@ -273,6 +276,7 @@ watch(
|
||||
() => inProps.path,
|
||||
async () => {
|
||||
items.value = []
|
||||
nameTestResult.value = undefined
|
||||
await load()
|
||||
},
|
||||
)
|
||||
@@ -311,6 +315,34 @@ function stopLoadingProgress() {
|
||||
progressEventSource.value?.close()
|
||||
}
|
||||
|
||||
// 调用API识别
|
||||
async function recognize(path: string) {
|
||||
try {
|
||||
// 显示进度条
|
||||
progressDialog.value = true
|
||||
progressText.value = `正在识别 ${path} ...`
|
||||
nameTestResult.value = await api.get('media/recognize_file', {
|
||||
params: {
|
||||
path,
|
||||
},
|
||||
})
|
||||
// 关闭进度条
|
||||
progressDialog.value = false
|
||||
if (!nameTestResult.value)
|
||||
$toast.error(`${path} 识别失败!`)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// TMDB图片转换为w500大小
|
||||
function getW500Image(url = '') {
|
||||
if (!url)
|
||||
return ''
|
||||
return url.replace('original', 'w500')
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
@@ -365,9 +397,118 @@ onMounted(() => {
|
||||
</VCardText>
|
||||
<VCardText
|
||||
v-else-if="isFile && !isImage"
|
||||
class="grow d-flex justify-center align-center break-all"
|
||||
class="text-center break-all"
|
||||
>
|
||||
文件: {{ path }}<br>
|
||||
文件: {{ path }}
|
||||
<VDivider v-if="nameTestResult" class="my-3" />
|
||||
<div
|
||||
v-if="nameTestResult?.meta_info?.name"
|
||||
class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row"
|
||||
>
|
||||
<div
|
||||
v-if="nameTestResult?.media_info?.poster_path"
|
||||
class="ma-auto"
|
||||
>
|
||||
<VImg
|
||||
width="10rem"
|
||||
aspect-ratio="2/3"
|
||||
class="object-cover aspect-w-2 aspect-h-3 rounded-lg ring-1 ring-gray-500"
|
||||
:src="getW500Image(nameTestResult?.media_info?.poster_path)"
|
||||
cover
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
|
||||
<div class="text-start">
|
||||
<VCardItem class="pb-1">
|
||||
<VCardTitle>
|
||||
{{ nameTestResult?.media_info?.title || nameTestResult?.meta_info?.name }}
|
||||
{{ nameTestResult?.meta_info?.season_episode }}
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>
|
||||
{{ nameTestResult?.media_info?.year || nameTestResult?.meta_info?.year }}
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText
|
||||
v-if="nameTestResult?.media_info?.overview"
|
||||
class="line-clamp-4 overflow-hidden text-ellipsis ..."
|
||||
>
|
||||
{{ nameTestResult?.media_info?.overview }}
|
||||
</VCardText>
|
||||
|
||||
<VCardItem>
|
||||
<!-- 类型 -->
|
||||
<VChip
|
||||
v-if="nameTestResult?.media_info?.type || nameTestResult?.meta_info?.type"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-blue-500"
|
||||
>
|
||||
{{
|
||||
nameTestResult?.media_info?.type || nameTestResult?.meta_info?.type
|
||||
}}
|
||||
</VChip>
|
||||
<!-- 二级分类 -->
|
||||
<VChip
|
||||
v-if="nameTestResult?.media_info?.category"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-blue-500"
|
||||
>
|
||||
{{ nameTestResult?.media_info?.category }}
|
||||
</VChip>
|
||||
<!-- TMDBID -->
|
||||
<VChip
|
||||
v-if="nameTestResult?.media_info?.tmdb_id"
|
||||
variant="elevated"
|
||||
color="success"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ nameTestResult?.media_info?.tmdb_id }}
|
||||
</VChip>
|
||||
<!-- meta_info -->
|
||||
<VChip
|
||||
v-if="nameTestResult?.meta_info?.edition"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
{{ nameTestResult?.meta_info?.edition }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="nameTestResult?.meta_info?.resource_pix"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
{{ nameTestResult?.meta_info?.resource_pix }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="nameTestResult?.meta_info?.video_encode"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-orange-500"
|
||||
>
|
||||
{{ nameTestResult?.meta_info?.video_encode }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="nameTestResult?.meta_info?.audio_encode"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-orange-500"
|
||||
>
|
||||
{{ nameTestResult?.meta_info?.audio_encode }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="nameTestResult?.meta_info?.resource_team"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-cyan-500"
|
||||
>
|
||||
{{ nameTestResult?.meta_info?.resource_team }}
|
||||
</VChip>
|
||||
</VCardItem>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText
|
||||
v-else-if="isFile && isImage"
|
||||
@@ -505,12 +646,21 @@ onMounted(() => {
|
||||
class="me-2"
|
||||
/>
|
||||
<VSpacer v-if="isFile" />
|
||||
<VBtn v-if="isFile" @click="download(inProps.path || '')">
|
||||
<VIcon>mdi-download</VIcon>
|
||||
</VBtn>
|
||||
<VBtn v-if="!isFile" @click="load">
|
||||
<VIcon>mdi-refresh</VIcon>
|
||||
</VBtn>
|
||||
<IconBtn v-if="isFile" @click="recognize(inProps.path || '')">
|
||||
<VIcon color="primary">
|
||||
mdi-text-recognition
|
||||
</VIcon>
|
||||
</IconBtn>
|
||||
<IconBtn v-if="isFile" @click="download(inProps.path || '')">
|
||||
<VIcon color="primary">
|
||||
mdi-download
|
||||
</VIcon>
|
||||
</IconBtn>
|
||||
<IconBtn v-if="!isFile" @click="load">
|
||||
<VIcon color="primary">
|
||||
mdi-refresh
|
||||
</VIcon>
|
||||
</IconBtn>
|
||||
</VToolbar>
|
||||
</VCard>
|
||||
<!-- 重命名弹窗 -->
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import NameTestView from '@/views/system/NameTestView.vue'
|
||||
import NetTestView from '@/views/system/NetTestView.vue'
|
||||
import LoggingView from '@/views/system/LoggingView.vue'
|
||||
|
||||
// App捷径
|
||||
const appsMenu = ref(false)
|
||||
@@ -10,6 +11,9 @@ const nameTestDialog = ref(false)
|
||||
|
||||
// 网络测试弹窗
|
||||
const netTestDialog = ref(false)
|
||||
|
||||
// 实时日志弹窗
|
||||
const loggingDialog = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -86,6 +90,29 @@ const netTestDialog = ref(false)
|
||||
</VListItem>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow class="ma-0 mt-n1 border-t">
|
||||
<VCol
|
||||
cols="6"
|
||||
class="text-center cursor-pointer pa-0 shortcut-icon border-e"
|
||||
@click="() => {}"
|
||||
>
|
||||
<VListItem
|
||||
class="pa-4"
|
||||
@click="loggingDialog = true"
|
||||
>
|
||||
<VAvatar
|
||||
size="48"
|
||||
variant="tonal"
|
||||
>
|
||||
<VIcon icon="mdi-file-document-outline" />
|
||||
</VAvatar>
|
||||
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
||||
日志
|
||||
</h6>
|
||||
<span class="text-sm">系统实时日志</span>
|
||||
</VListItem>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VCard>
|
||||
</VMenu>
|
||||
@@ -113,4 +140,17 @@ const netTestDialog = ref(false)
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 实时日志弹窗 -->
|
||||
<VDialog
|
||||
v-model="loggingDialog"
|
||||
max-width="1280"
|
||||
scrollable
|
||||
>
|
||||
<VCard title="实时日志">
|
||||
<DialogCloseBtn @click="loggingDialog = false" />
|
||||
<VCardText>
|
||||
<LoggingView />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -5,7 +5,6 @@ import AccountSettingNotification from '@/views/setting/AccountSettingNotificati
|
||||
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
|
||||
import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
|
||||
import AccountSettingWords from '@/views/setting/AccountSettingWords.vue'
|
||||
import AccountSettingLogging from '@/views/setting/AccountSettingLogging.vue'
|
||||
import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -39,11 +38,6 @@ const tabs = [
|
||||
icon: 'mdi-file-word-box',
|
||||
tab: 'words',
|
||||
},
|
||||
{
|
||||
title: '日志',
|
||||
icon: 'mdi-text-box',
|
||||
tab: 'logging',
|
||||
},
|
||||
{
|
||||
title: '关于',
|
||||
icon: 'mdi-information',
|
||||
@@ -96,12 +90,6 @@ const tabs = [
|
||||
<AccountSettingWords />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
<!-- Logging -->
|
||||
<VWindowItem value="logging">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingLogging />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
<!-- About -->
|
||||
<VWindowItem value="about">
|
||||
<transition name="fade-slide" appear>
|
||||
|
||||
@@ -67,9 +67,9 @@ const theme: VuetifyOptions['theme'] = {
|
||||
'on-primary': '#FFFFFF',
|
||||
'on-success': '#FFFFFF',
|
||||
'on-warning': '#FFFFFF',
|
||||
'background': '#28243D',
|
||||
'background': '#111827',
|
||||
'on-background': '#E7E3FC',
|
||||
'surface': '#312D4B',
|
||||
'surface': '#161D2C',
|
||||
'on-surface': '#E7E3FC',
|
||||
'grey-50': '#2A2E42',
|
||||
'grey-100': '#474360',
|
||||
@@ -87,7 +87,7 @@ const theme: VuetifyOptions['theme'] = {
|
||||
},
|
||||
variables: {
|
||||
'code-color': '#d400ff',
|
||||
'overlay-scrim-background': '#2C2942',
|
||||
'overlay-scrim-background': '#1F2937',
|
||||
'overlay-scrim-opacity': 0.6,
|
||||
'hover-opacity': 0.04,
|
||||
'focus-opacity': 0.1,
|
||||
@@ -96,9 +96,8 @@ const theme: VuetifyOptions['theme'] = {
|
||||
'pressed-opacity': 0.14,
|
||||
'dragged-opacity': 0.1,
|
||||
'border-color': '#E7E3FC',
|
||||
'table-header-background': '#3D3759',
|
||||
'table-header-background': '#1F2937',
|
||||
'custom-background': '#373452',
|
||||
|
||||
// Shadows
|
||||
'shadow-key-umbra-opacity': 'rgba(20, 18, 33, 0.08)',
|
||||
'shadow-key-penumbra-opacity': 'rgba(20, 18, 33, 0.12)',
|
||||
|
||||
@@ -112,3 +112,14 @@
|
||||
background: #a1a1a1;
|
||||
}
|
||||
}
|
||||
|
||||
.v-alert--variant-elevated, .v-alert--variant-flat {
|
||||
background: rgb(var(--v-table-header-background));
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
|
||||
.backdrop-blur {
|
||||
--tw-backdrop-blur: blur(8px)!important;
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)!important;
|
||||
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)!important;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import _ from 'lodash'
|
||||
import api from '@/api'
|
||||
import type { Context } from '@/api/types'
|
||||
import TorrentCard from '@/components/cards/TorrentCard.vue'
|
||||
@@ -111,7 +112,7 @@ watchEffect(() => {
|
||||
)
|
||||
})
|
||||
if (matchData.length > 0) {
|
||||
const firstData = matchData[0] as SearchTorrent
|
||||
const firstData = _.cloneDeepWith(matchData[0]) as SearchTorrent
|
||||
if (matchData.length > 1)
|
||||
firstData.more = matchData.slice(1)
|
||||
|
||||
@@ -312,7 +313,7 @@ onMounted(initData)
|
||||
<span>{{ progressText }}</span>
|
||||
</div>
|
||||
<div v-if="dataList.length > 0" class="grid gap-3 grid-torrent-card items-start">
|
||||
<TorrentCard v-for="data in dataList" :key="`${data.torrent_info.title}_${data.torrent_info.site}`" :torrent="data" :more="data.more" />
|
||||
<TorrentCard v-for="data in dataList" :key="`${data.torrent_info.title}_${data.torrent_info.site_name}_${data.torrent_info.page_url}`" :torrent="data" :more="data.more" />
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
|
||||
@@ -30,6 +30,9 @@ const redoTypeItems = ref([
|
||||
// 当前操作记录
|
||||
const currentHistory = ref<TransferHistory>()
|
||||
|
||||
// 已选中的数据
|
||||
const selected = ref<TransferHistory[]>([])
|
||||
|
||||
// 表头
|
||||
const headers = [
|
||||
{ title: '标题', key: 'title', sortable: false },
|
||||
@@ -59,6 +62,15 @@ const itemsPerPage = ref(25)
|
||||
// 当前页码
|
||||
const currentPage = ref(1)
|
||||
|
||||
// 进度条
|
||||
const progressDialog = ref(false)
|
||||
|
||||
// 进度文本
|
||||
const progressText = ref('请稍候 ...')
|
||||
|
||||
// 进度值
|
||||
const progressValue = ref(0)
|
||||
|
||||
// 获取订阅列表数据
|
||||
async function fetchData({
|
||||
page,
|
||||
@@ -113,21 +125,30 @@ const TransferDict: { [key: string]: string } = {
|
||||
|
||||
// 删除历史记录
|
||||
async function removeHistory(item: TransferHistory) {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `同步删除 ${item.title} 对应的媒体库文件 ?`,
|
||||
confirmationText: '同步删除文件',
|
||||
cancellationText: '仅删除历史记录',
|
||||
dialogProps: {
|
||||
maxWidth: 600,
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
color: 'error',
|
||||
},
|
||||
})
|
||||
if (isConfirmed === undefined)
|
||||
return
|
||||
|
||||
// 执行删除
|
||||
remove(item, isConfirmed || false)
|
||||
// 清空选中项
|
||||
selected.value = []
|
||||
}
|
||||
|
||||
// 调用API删除记录
|
||||
async function remove(item: TransferHistory, deleteFile: boolean) {
|
||||
try {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `同步删除 ${item.title} 对应的媒体库文件 ?`,
|
||||
confirmationText: '同步删除文件',
|
||||
cancellationText: '仅删除历史记录',
|
||||
dialogProps: {
|
||||
maxWidth: 600,
|
||||
},
|
||||
})
|
||||
|
||||
let deleteFile = false
|
||||
if (isConfirmed)
|
||||
deleteFile = true
|
||||
|
||||
// 调用删除API
|
||||
const result: { [key: string]: any } = await api.delete(`history/transfer?delete_file=${deleteFile}`, {
|
||||
data: item,
|
||||
@@ -148,6 +169,54 @@ async function removeHistory(item: TransferHistory) {
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除历史记录
|
||||
async function removeHistoryBatch() {
|
||||
if (selected.value.length === 0)
|
||||
return
|
||||
// 确认
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `同步删除 ${selected.value.length} 条记录对应的媒体库文件 ?`,
|
||||
confirmationText: '同步删除文件',
|
||||
cancellationText: '仅删除历史记录',
|
||||
dialogProps: {
|
||||
maxWidth: 600,
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
color: 'error',
|
||||
},
|
||||
})
|
||||
if (isConfirmed === undefined)
|
||||
return
|
||||
|
||||
console.log(selected.value)
|
||||
|
||||
// 总条数
|
||||
const total = selected.value.length
|
||||
// 已处理条数
|
||||
let handled = 0
|
||||
// 显示进度条
|
||||
progressDialog.value = true
|
||||
// 循环调用removeHistory
|
||||
for (const item of selected.value) {
|
||||
// 开始删除
|
||||
progressText.value = `正在删除 ${item.title} ${item.seasons}${item.episodes} ...`
|
||||
await remove(item, isConfirmed || false)
|
||||
// 删除完成
|
||||
handled++
|
||||
progressValue.value = handled / total * 100
|
||||
}
|
||||
// 清空选中项
|
||||
selected.value = []
|
||||
// 隐藏进度条
|
||||
progressDialog.value = false
|
||||
// 重新获取数据
|
||||
fetchData({
|
||||
page: currentPage.value,
|
||||
itemsPerPage: itemsPerPage.value,
|
||||
})
|
||||
}
|
||||
|
||||
// 重新整理
|
||||
async function rehandleHistory() {
|
||||
try {
|
||||
@@ -238,6 +307,7 @@ const dropdownItems = ref([
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDataTableServer
|
||||
v-model="selected"
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
:headers="headers"
|
||||
:items="dataList"
|
||||
@@ -248,6 +318,7 @@ const dropdownItems = ref([
|
||||
item-value="id"
|
||||
return-object
|
||||
fixed-header
|
||||
show-select
|
||||
items-per-page-text="每页条数"
|
||||
page-text="{0}-{1} 共 {2} 条"
|
||||
@update:options="fetchData"
|
||||
@@ -357,6 +428,33 @@ const dropdownItems = ref([
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<span v-if="selected.length > 0" class="fixed right-5 bottom-5">
|
||||
<VBtn
|
||||
icon="mdi-trash-can-outline"
|
||||
color="error"
|
||||
size="x-large"
|
||||
@click="removeHistoryBatch"
|
||||
/>
|
||||
</span>
|
||||
<!-- 进度框 -->
|
||||
<vDialog
|
||||
v-model="progressDialog"
|
||||
:scrim="false"
|
||||
width="400"
|
||||
>
|
||||
<vCard
|
||||
color="primary"
|
||||
>
|
||||
<vCardText class="text-center">
|
||||
{{ progressText }}
|
||||
<vProgressLinear
|
||||
color="white"
|
||||
class="mb-0 mt-1"
|
||||
:model-value="progressValue"
|
||||
/>
|
||||
</vCardText>
|
||||
</vCard>
|
||||
</vDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import store from '@/store'
|
||||
|
||||
// 日志列表
|
||||
const logs = ref<string[]>([])
|
||||
|
||||
// SSE持续获取日志
|
||||
function startSSELogging() {
|
||||
const token = store.state.auth.token
|
||||
if (token) {
|
||||
const eventSource = new EventSource(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}`,
|
||||
)
|
||||
|
||||
eventSource.addEventListener('message', (event) => {
|
||||
const message = event.data
|
||||
if (message)
|
||||
logs.value.push(message)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
eventSource.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startSSELogging()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="实时日志">
|
||||
<VCardText>
|
||||
<div
|
||||
v-if="logs.length === 0"
|
||||
class="mt-5 w-full text-center text-gray-500 text-sm flex flex-col items-center"
|
||||
>
|
||||
<VProgressCircular
|
||||
size="48"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
<span class="mt-3">正在刷新 ...</span>
|
||||
</div>
|
||||
<div v-for="(log, i) in logs" :key="i">
|
||||
{{ log }}
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
@@ -11,6 +11,9 @@ const customIdentifiers = ref('')
|
||||
// 自定义制作组
|
||||
const customReleaseGroups = ref('')
|
||||
|
||||
// 文件整理屏蔽词
|
||||
const transferExcludeWords = ref('')
|
||||
|
||||
// 查询已设置的识别词
|
||||
async function queryCustomIdentifiers() {
|
||||
try {
|
||||
@@ -39,6 +42,20 @@ async function queryCustomReleaseGroups() {
|
||||
}
|
||||
}
|
||||
|
||||
// 查询已设置的屏蔽词
|
||||
async function queryTransferExcludeWords() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/TransferExcludeWords',
|
||||
)
|
||||
|
||||
transferExcludeWords.value = result.data?.value.join('\n')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户设置的识别词
|
||||
async function saveCustomIdentifiers() {
|
||||
try {
|
||||
@@ -77,9 +94,29 @@ async function saveCustomReleaseGroups() {
|
||||
}
|
||||
}
|
||||
|
||||
// 保存文件整理屏蔽词
|
||||
async function saveTransferExcludeWords() {
|
||||
try {
|
||||
// 用户名密码
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/setting/TransferExcludeWords',
|
||||
transferExcludeWords.value.split('\n'),
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('文件整理屏蔽词保存成功')
|
||||
else
|
||||
$toast.error('文件整理屏蔽词保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
queryCustomIdentifiers()
|
||||
queryCustomReleaseGroups()
|
||||
queryTransferExcludeWords()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -128,5 +165,25 @@ onMounted(() => {
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="文件整理屏蔽词">
|
||||
<VCardSubtitle> 目录名或文件名中包含屏蔽词时不进行整理 </VCardSubtitle>
|
||||
<VCardItem>
|
||||
<VTextarea
|
||||
v-model="transferExcludeWords"
|
||||
auto-grow
|
||||
placeholder="支持正则表达式,特殊字符需要\转义,一行代表一个屏蔽词"
|
||||
/>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveTransferExcludeWords"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CalendarOptions } from '@fullcalendar/core'
|
||||
import type { CalendarOptions, EventSourceInput } from '@fullcalendar/core'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
@@ -89,7 +89,7 @@ async function getSubscribes() {
|
||||
// 合并事件
|
||||
const events = [...subEvents, ...rssEvents]
|
||||
|
||||
calendarOptions.value.events = events.flat()
|
||||
calendarOptions.value.events = events.flat().filter(event => event.start) as EventSourceInput
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
|
||||
119
src/views/system/LoggingView.vue
Normal file
119
src/views/system/LoggingView.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<script lang="ts" setup>
|
||||
import store from '@/store'
|
||||
|
||||
// 日志列表
|
||||
const logs = ref<string[]>([])
|
||||
|
||||
// SSE持续获取日志
|
||||
function startSSELogging() {
|
||||
const token = store.state.auth.token
|
||||
if (token) {
|
||||
const eventSource = new EventSource(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}`,
|
||||
)
|
||||
|
||||
eventSource.addEventListener('message', (event) => {
|
||||
const message = event.data
|
||||
if (message)
|
||||
logs.value.push(message)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
eventSource.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 从日志中提取日志详情
|
||||
function extractLogDetailsFromLogs(logs: string[]): { level: string; time: string; program: string; content: string }[] {
|
||||
const logDetails: { level: string; time: string; program: string; content: string }[] = []
|
||||
|
||||
const logPattern = /^【(.*?)】.*\s(.*?)\s-\s(.*?)\s-\s(.*)$/
|
||||
|
||||
for (const log of logs) {
|
||||
const matches = RegExp(logPattern).exec(log)
|
||||
if (matches && matches.length === 5) {
|
||||
const [_, level, time, program, content] = matches
|
||||
logDetails.push({ level, time, program, content })
|
||||
}
|
||||
}
|
||||
|
||||
return logDetails
|
||||
}
|
||||
|
||||
// 计算日志颜色
|
||||
function getLogColor(level: string): string {
|
||||
switch (level) {
|
||||
case 'DEBUG':
|
||||
return 'primary'
|
||||
case 'INFO':
|
||||
return 'secondary'
|
||||
case 'WARNING':
|
||||
return 'warning'
|
||||
case 'ERROR':
|
||||
return 'error'
|
||||
default:
|
||||
return 'secondary'
|
||||
}
|
||||
}
|
||||
|
||||
// 拆分日志数据计算属性
|
||||
const extractLogDetails = computed(() => {
|
||||
return extractLogDetailsFromLogs(logs.value)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
startSSELogging()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="logs.length === 0"
|
||||
class="mt-5 w-full text-centerflex flex-col items-center"
|
||||
>
|
||||
<VProgressCircular
|
||||
size="48"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
<span class="mt-3">正在刷新 ...</span>
|
||||
</div>
|
||||
<VTable
|
||||
class="table-rounded"
|
||||
hide-default-footer
|
||||
disable-sort
|
||||
>
|
||||
<tbody>
|
||||
<tr v-for="(log, i) in extractLogDetails" :key="i" class="text-sm">
|
||||
<td
|
||||
class="text-sm"
|
||||
>
|
||||
<VChip
|
||||
size="small"
|
||||
:color="getLogColor(log.level)"
|
||||
variant="elevated"
|
||||
v-text="log.level"
|
||||
/>
|
||||
</td>
|
||||
<!-- name -->
|
||||
<td
|
||||
class="text-sm"
|
||||
>
|
||||
{{ log.time }}
|
||||
</td>
|
||||
<td
|
||||
class="text-sm"
|
||||
>
|
||||
<h6 class="text-sm font-weight-medium">
|
||||
{{ log.program }}
|
||||
</h6>
|
||||
</td>
|
||||
<td
|
||||
class="text-sm"
|
||||
v-text="log.content"
|
||||
/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</template>
|
||||
Reference in New Issue
Block a user