Compare commits

...

47 Commits

Author SHA1 Message Date
jxxghp
31047b0d44 优化账户设置缓存页面的筛选条件 2025-05-30 17:01:47 +08:00
jxxghp
7c2b724d10 fix ui 2025-05-30 09:04:15 +08:00
jxxghp
ca5670f06b v2.5.2 2025-05-30 08:48:39 +08:00
jxxghp
427e05871d 调整SubscribeCard组件的样式 2025-05-30 08:32:16 +08:00
jxxghp
bef56bdb56 优化账户设置缓存页面中的输入字段,添加持久提示和图标,提升用户体验 2025-05-30 08:27:10 +08:00
jxxghp
d450d02e18 在账户设置缓存页面中添加固定表头 2025-05-30 08:25:03 +08:00
jxxghp
85a766cc7b 调整多个组件的样式和结构,优化用户界面体验 2025-05-30 08:15:48 +08:00
jxxghp
a473f356c9 优化缓存管理页面 2025-05-29 22:56:40 +08:00
jxxghp
52b5fdf383 添加清空缓存确认提示,优化缓存管理页面的用户体验 2025-05-29 22:37:03 +08:00
jxxghp
b886f02043 缓存管理页面 2025-05-29 20:49:19 +08:00
jxxghp
61963ea497 reset 2025-05-29 20:12:14 +08:00
jxxghp
2f9b27ad9e reset 2025-05-29 20:11:34 +08:00
jxxghp
9334109767 Merge pull request #341 from madrays/v2
增加缓存管理页面
2025-05-29 12:32:51 +08:00
jxxghp
2bc52576d9 更新package.json中的版本号 2025-05-29 08:23:18 +08:00
jxxghp
700d2c4a51 刷新数据时重新加载文件夹配置,以确保插件正确显示。 2025-05-29 08:21:17 +08:00
madrays
103bdb32c8 增加缓存管理页面 2025-05-29 00:45:12 +08:00
jxxghp
92b745e180 优化搜索站点对话框 2025-05-28 21:25:37 +08:00
jxxghp
a2007083b8 更新MoviePilot自动更新设置逻辑,支持'release'和'dev'选项 2025-05-28 21:15:52 +08:00
jxxghp
36a5f7ff29 添加自动更新MoviePilot和站点资源的设置选项 2025-05-28 21:05:46 +08:00
jxxghp
f727aea51d 为多个设置组件的保存按钮添加图标,以提升用户体验和一致性。 2025-05-28 10:09:05 +08:00
jxxghp
936ca24328 优化对话框组件,添加图标以提升用户体验 2025-05-28 08:59:31 +08:00
jxxghp
62f49b6087 优化插件文件夹内插件的筛选逻辑 2025-05-28 08:49:53 +08:00
jxxghp
e9ddbf9962 添加代理服务器设置 2025-05-28 08:24:42 +08:00
jxxghp
196cf522e6 fix 2025-05-27 21:41:06 +08:00
jxxghp
3fce3bf4a7 优化多个组件的输入框,添加图标以提升用户体验,确保提示信息的一致性和可读性。 2025-05-27 21:38:25 +08:00
jxxghp
1cfee25695 优化多个组件的输入框,添加图标以提升用户体验,确保提示信息的一致性和可读性。 2025-05-27 21:23:08 +08:00
jxxghp
5711285a77 更新多个卡片组件,统一标题文本为“配置”,添加图标以提升用户体验,优化输入框提示信息,确保一致性和可读性。 2025-05-27 17:46:51 +08:00
jxxghp
e6f537ca3a 优化多个对话框组件的布局,添加图标以提升用户体验,调整部分文本提示,确保一致性和可读性。 2025-05-27 17:40:20 +08:00
jxxghp
3b5220af57 fix plugin list loading 2025-05-27 14:00:15 +08:00
jxxghp
fa6b4b1d2d 调整插件列表显示行数,从三行改为两行,以优化界面布局。 2025-05-27 13:49:55 +08:00
jxxghp
7968e5374b 优化文件夹内插件的显示顺序,确保按照保存顺序排列插件,提升用户体验。 2025-05-27 13:48:13 +08:00
jxxghp
64997ebe45 重构插件混合排序逻辑,优化全局排序配置,兼容旧格式,提升插件和文件夹的排序体验。 2025-05-27 13:40:55 +08:00
jxxghp
f8592b01e2 优化错误日志输出 2025-05-27 13:29:53 +08:00
jxxghp
087474f514 fix 2025-05-27 13:26:09 +08:00
jxxghp
1725088f05 fix 插件混合排序问题 2025-05-27 13:12:09 +08:00
jxxghp
ec1b756a3d 添加混合排序功能,重构插件列表显示逻辑,移除冗余代码并优化拖拽排序体验。 2025-05-27 13:01:08 +08:00
jxxghp
76a06e0817 移除 AddDownloadDialog 组件中的显示器宽度逻辑,简化对话框全屏显示设置 2025-05-27 07:54:34 +08:00
jxxghp
02fb608d7b 更新 PluginCard.vue 2025-05-26 22:40:48 +08:00
jxxghp
e17fc2fc12 更新 package.json 2025-05-26 21:38:10 +08:00
jxxghp
4f6c317652 修复 PersonDetailView 组件中的 VImg 标签,移除多余的 v-img 指令以简化代码。 2025-05-26 21:30:23 +08:00
jxxghp
46c198be26 重构 credits.vue 和 media.vue 组件,简化 API 路径处理,移除不必要的路由参数,同时优化 PersonCardListView 组件的样式。 2025-05-26 21:28:52 +08:00
jxxghp
8552203d43 PluginCard 组件中的实时日志弹窗代码 2025-05-26 13:26:13 +08:00
jxxghp
139eaa7016 优化 PluginCard 组件 2025-05-26 12:44:08 +08:00
jxxghp
d81120ab8f 为 PluginCard 组件添加实时日志弹窗功能 2025-05-26 12:37:49 +08:00
jxxghp
6353d56beb Merge pull request #339 from madrays/v2 2025-05-26 11:26:26 +08:00
madrays
aa05496b42 插件分身多语言支持 2025-05-26 11:20:10 +08:00
madrays
dc15e537d8 增加插件分身功能 2025-05-26 10:55:55 +08:00
58 changed files with 2263 additions and 472 deletions

View File

@@ -110,4 +110,4 @@
"i18n-ally.localesPaths": [ "i18n-ally.localesPaths": [
"src/locales" "src/locales"
] ]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "moviepilot", "name": "moviepilot",
"version": "2.5.0", "version": "2.5.2",
"private": true, "private": true,
"type": "module", "type": "module",
"bin": "dist/service.js", "bin": "dist/service.js",

View File

@@ -1305,3 +1305,49 @@ export interface Workflow {
// 最后执行时间 // 最后执行时间
last_time?: string last_time?: string
} }
// 种子缓存项
export interface TorrentCacheItem {
// 种子hash用于操作标识
hash: string
// 站点域名
domain: string
// 种子标题
title: string
// 种子描述
description?: string
// 种子大小
size: number
// 发布时间
pubdate?: string
// 站点名称
site_name?: string
// 识别的媒体名称
media_name?: string
// 识别的媒体年份
media_year?: string
// 识别的媒体类型
media_type?: string
// 季集信息
season_episode?: string
// 资源信息
resource_term?: string
// 种子链接
enclosure?: string
// 详情页面
page_url?: string
// 海报图片
poster_path?: string
// 背景图片
backdrop_path?: string
}
// 种子缓存数据
export interface TorrentCacheData {
// 缓存数量
count: number
// 站点数量
sites: number
// 缓存数据
data: TorrentCacheItem[]
}

View File

@@ -117,7 +117,13 @@ function onClose() {
max-width="40rem" max-width="40rem"
:fullscreen="!display.mdAndUp.value" :fullscreen="!display.mdAndUp.value"
> >
<VCard :title="t('customRule.title', { id: props.rule.id })"> <VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-filter-outline" class="me-2" />
</template>
<VCardTitle>{{ t('customRule.title', { id: props.rule.id }) }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn v-model="ruleInfoDialog" /> <VDialogCloseBtn v-model="ruleInfoDialog" />
<VDivider /> <VDivider />
<VCardText> <VCardText>
@@ -131,6 +137,7 @@ function onClose() {
:hint="t('customRule.hint.ruleId')" :hint="t('customRule.hint.ruleId')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-identifier"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -141,6 +148,7 @@ function onClose() {
:hint="t('customRule.hint.ruleName')" :hint="t('customRule.hint.ruleName')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-label"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
@@ -151,6 +159,7 @@ function onClose() {
:hint="t('customRule.hint.include')" :hint="t('customRule.hint.include')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-plus-circle"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
@@ -161,6 +170,7 @@ function onClose() {
:hint="t('customRule.hint.exclude')" :hint="t('customRule.hint.exclude')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-minus-circle"
/> />
</VCol> </VCol>
<VCol cols="6"> <VCol cols="6">
@@ -171,6 +181,7 @@ function onClose() {
:hint="t('customRule.hint.sizeRange')" :hint="t('customRule.hint.sizeRange')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-harddisk"
/> />
</VCol> </VCol>
<VCol cols="6"> <VCol cols="6">
@@ -181,6 +192,7 @@ function onClose() {
:hint="t('customRule.hint.seeders')" :hint="t('customRule.hint.seeders')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-account-group"
/> />
</VCol> </VCol>
<VCol cols="6"> <VCol cols="6">
@@ -191,6 +203,7 @@ function onClose() {
:hint="t('customRule.hint.publishTime')" :hint="t('customRule.hint.publishTime')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-calendar-clock"
/> />
</VCol> </VCol>
</VRow> </VRow>

View File

@@ -200,7 +200,14 @@ onUnmounted(() => {
max-width="40rem" max-width="40rem"
:fullscreen="!display.mdAndUp.value" :fullscreen="!display.mdAndUp.value"
> >
<VCard :title="`${props.downloader.name} - ${t('downloader.title')}`"> <VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-download" class="me-2" />
</template>
<VCardTitle>{{ t('common.config') }}</VCardTitle>
<VCardSubtitle>{{ props.downloader.name }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn v-model="downloaderInfoDialog" /> <VDialogCloseBtn v-model="downloaderInfoDialog" />
<VDivider /> <VDivider />
<VCardText> <VCardText>
@@ -226,6 +233,7 @@ onUnmounted(() => {
:hint="t('downloader.name')" :hint="t('downloader.name')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-label"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -236,6 +244,7 @@ onUnmounted(() => {
:hint="t('downloader.host')" :hint="t('downloader.host')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-server"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -245,6 +254,7 @@ onUnmounted(() => {
:hint="t('downloader.username')" :hint="t('downloader.username')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-account"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -255,6 +265,7 @@ onUnmounted(() => {
:hint="t('downloader.password')" :hint="t('downloader.password')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-lock"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -303,6 +314,7 @@ onUnmounted(() => {
:hint="t('downloader.name')" :hint="t('downloader.name')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-label"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -313,6 +325,7 @@ onUnmounted(() => {
:hint="t('downloader.host')" :hint="t('downloader.host')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-server"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -322,6 +335,7 @@ onUnmounted(() => {
:hint="t('downloader.username')" :hint="t('downloader.username')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-account"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -332,6 +346,7 @@ onUnmounted(() => {
:hint="t('downloader.password')" :hint="t('downloader.password')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-lock"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -343,6 +358,7 @@ onUnmounted(() => {
:hint="t('downloader.customTypeHint')" :hint="t('downloader.customTypeHint')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-cog"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -352,6 +368,7 @@ onUnmounted(() => {
:hint="t('downloader.nameRequired')" :hint="t('downloader.nameRequired')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-label"
/> />
</VCol> </VCol>
</VRow> </VRow>

View File

@@ -243,6 +243,7 @@ function onClose() {
:hint="t('filterRule.groupName')" :hint="t('filterRule.groupName')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-label"
/> />
</VCol> </VCol>
<VCol cols="6" md="3"> <VCol cols="6" md="3">
@@ -253,6 +254,7 @@ function onClose() {
:hint="t('filterRule.mediaType')" :hint="t('filterRule.mediaType')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-movie-open"
/> />
</VCol> </VCol>
<VCol cols="6" md="3"> <VCol cols="6" md="3">
@@ -263,6 +265,7 @@ function onClose() {
:hint="t('filterRule.category')" :hint="t('filterRule.category')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-folder-open"
/> />
</VCol> </VCol>
</VRow> </VRow>

View File

@@ -211,7 +211,14 @@ onMounted(() => {
max-width="40rem" max-width="40rem"
:fullscreen="!display.mdAndUp.value" :fullscreen="!display.mdAndUp.value"
> >
<VCard :title="`${props.mediaserver.name} - ${t('common.config')}`"> <VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-cog" class="me-2" />
</template>
<VCardTitle>{{ t('common.config') }}</VCardTitle>
<VCardSubtitle>{{ props.mediaserver.name }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn v-model="mediaServerInfoDialog" /> <VDialogCloseBtn v-model="mediaServerInfoDialog" />
<VDivider /> <VDivider />
<VCardText> <VCardText>
@@ -230,6 +237,7 @@ onMounted(() => {
:hint="t('mediaserver.serverAlias')" :hint="t('mediaserver.serverAlias')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-label"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -240,6 +248,7 @@ onMounted(() => {
:hint="t('mediaserver.hostHint')" :hint="t('mediaserver.hostHint')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-server"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -250,6 +259,7 @@ onMounted(() => {
:hint="t('mediaserver.playHostHint')" :hint="t('mediaserver.playHostHint')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-play-network"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -259,6 +269,7 @@ onMounted(() => {
:hint="t('mediaserver.embyApiKeyHint')" :hint="t('mediaserver.embyApiKeyHint')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-key"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
@@ -273,6 +284,7 @@ onMounted(() => {
persistent-hint persistent-hint
active active
append-inner-icon="mdi-refresh" append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)" @click:append-inner="loadLibrary(mediaServerInfo.name)"
/> />
</VCol> </VCol>
@@ -286,6 +298,7 @@ onMounted(() => {
:hint="t('mediaserver.serverAlias')" :hint="t('mediaserver.serverAlias')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-label"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -296,6 +309,7 @@ onMounted(() => {
:hint="t('mediaserver.hostHint')" :hint="t('mediaserver.hostHint')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-server"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -306,6 +320,7 @@ onMounted(() => {
:hint="t('mediaserver.playHostHint')" :hint="t('mediaserver.playHostHint')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-play-network"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -315,6 +330,7 @@ onMounted(() => {
:hint="t('mediaserver.jellyfinApiKeyHint')" :hint="t('mediaserver.jellyfinApiKeyHint')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-key"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
@@ -329,6 +345,7 @@ onMounted(() => {
persistent-hint persistent-hint
active active
append-inner-icon="mdi-refresh" append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)" @click:append-inner="loadLibrary(mediaServerInfo.name)"
/> />
</VCol> </VCol>
@@ -342,6 +359,7 @@ onMounted(() => {
:hint="t('mediaserver.serverAlias')" :hint="t('mediaserver.serverAlias')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-label"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -352,6 +370,7 @@ onMounted(() => {
:hint="t('mediaserver.hostHint')" :hint="t('mediaserver.hostHint')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-server"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
@@ -362,10 +381,16 @@ onMounted(() => {
:hint="t('mediaserver.playHostHint')" :hint="t('mediaserver.playHostHint')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-play-network"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField v-model="mediaServerInfo.config.username" :label="t('mediaserver.username')" active /> <VTextField
v-model="mediaServerInfo.config.username"
:label="t('mediaserver.username')"
active
prepend-inner-icon="mdi-account"
/>
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
@@ -373,6 +398,7 @@ onMounted(() => {
v-model="mediaServerInfo.config.password" v-model="mediaServerInfo.config.password"
:label="t('mediaserver.password')" :label="t('mediaserver.password')"
active active
prepend-inner-icon="mdi-lock"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
@@ -387,6 +413,7 @@ onMounted(() => {
persistent-hint persistent-hint
active active
append-inner-icon="mdi-refresh" append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)" @click:append-inner="loadLibrary(mediaServerInfo.name)"
/> />
</VCol> </VCol>
@@ -400,6 +427,7 @@ onMounted(() => {
:hint="t('mediaserver.serverAlias')" :hint="t('mediaserver.serverAlias')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-label"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -410,6 +438,7 @@ onMounted(() => {
:hint="t('mediaserver.hostHint')" :hint="t('mediaserver.hostHint')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-server"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -420,6 +449,7 @@ onMounted(() => {
:hint="t('mediaserver.playHostHint')" :hint="t('mediaserver.playHostHint')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-play-network"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -429,6 +459,7 @@ onMounted(() => {
:hint="t('mediaserver.plexTokenHint')" :hint="t('mediaserver.plexTokenHint')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-key"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
@@ -443,6 +474,7 @@ onMounted(() => {
persistent-hint persistent-hint
active active
append-inner-icon="mdi-refresh" append-inner-icon="mdi-refresh"
prepend-inner-icon="mdi-library"
@click:append-inner="loadLibrary(mediaServerInfo.name)" @click:append-inner="loadLibrary(mediaServerInfo.name)"
/> />
</VCol> </VCol>
@@ -454,10 +486,16 @@ onMounted(() => {
:label="t('mediaserver.type')" :label="t('mediaserver.type')"
:hint="t('mediaserver.customTypeHint')" :hint="t('mediaserver.customTypeHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-cog"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField :label="t('common.name')" :hint="t('mediaserver.nameRequired')" persistent-hint /> <VTextField
:label="t('common.name')"
:hint="t('mediaserver.nameRequired')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol> </VCol>
</VRow> </VRow>
</VForm> </VForm>

View File

@@ -148,8 +148,15 @@ function onClose() {
max-width="40rem" max-width="40rem"
:fullscreen="!display.mdAndUp.value" :fullscreen="!display.mdAndUp.value"
> >
<VCard :title="`${props.notification.name} - ${t('notification.config')}`"> <VCard>
<VDialogCloseBtn v-model="notificationInfoDialog" /> <VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-cog" class="me-2" />
</template>
<VCardTitle>{{ t('common.config') }}</VCardTitle>
<VCardSubtitle>{{ props.notification.name }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="notificationInfoDialog = false" />
<VDivider /> <VDivider />
<VCardText> <VCardText>
<VForm> <VForm>
@@ -167,6 +174,7 @@ function onClose() {
clearable clearable
chips chips
persistent-hint persistent-hint
prepend-inner-icon="mdi-bell-outline"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -178,6 +186,7 @@ function onClose() {
:placeholder="t('notification.name')" :placeholder="t('notification.name')"
:hint="t('notification.nameHint')" :hint="t('notification.nameHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-label"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -186,6 +195,7 @@ function onClose() {
:label="t('notification.wechat.corpId')" :label="t('notification.wechat.corpId')"
:hint="t('notification.wechat.corpIdHint')" :hint="t('notification.wechat.corpIdHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-domain"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -194,6 +204,7 @@ function onClose() {
:label="t('notification.wechat.appId')" :label="t('notification.wechat.appId')"
:hint="t('notification.wechat.appIdHint')" :hint="t('notification.wechat.appIdHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-application"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -202,6 +213,7 @@ function onClose() {
:label="t('notification.wechat.appSecret')" :label="t('notification.wechat.appSecret')"
:hint="t('notification.wechat.appSecretHint')" :hint="t('notification.wechat.appSecretHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-key"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -210,6 +222,7 @@ function onClose() {
:label="t('notification.wechat.proxy')" :label="t('notification.wechat.proxy')"
:hint="t('notification.wechat.proxyHint')" :hint="t('notification.wechat.proxyHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-server-network"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -218,6 +231,7 @@ function onClose() {
:label="t('notification.wechat.token')" :label="t('notification.wechat.token')"
:hint="t('notification.wechat.tokenHint')" :hint="t('notification.wechat.tokenHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-key-variant"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -226,6 +240,7 @@ function onClose() {
:label="t('notification.wechat.encodingAesKey')" :label="t('notification.wechat.encodingAesKey')"
:hint="t('notification.wechat.encodingAesKeyHint')" :hint="t('notification.wechat.encodingAesKeyHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-lock"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -235,6 +250,7 @@ function onClose() {
:placeholder="t('notification.wechat.adminsPlaceholder')" :placeholder="t('notification.wechat.adminsPlaceholder')"
:hint="t('notification.wechat.adminsHint')" :hint="t('notification.wechat.adminsHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-account-supervisor"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -246,6 +262,7 @@ function onClose() {
:placeholder="t('notification.name')" :placeholder="t('notification.name')"
:hint="t('notification.nameHint')" :hint="t('notification.nameHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-label"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -254,6 +271,7 @@ function onClose() {
:label="t('notification.telegram.token')" :label="t('notification.telegram.token')"
:hint="t('notification.telegram.tokenHint')" :hint="t('notification.telegram.tokenHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-key"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -262,6 +280,7 @@ function onClose() {
:label="t('notification.telegram.chatId')" :label="t('notification.telegram.chatId')"
:hint="t('notification.telegram.chatIdHint')" :hint="t('notification.telegram.chatIdHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-chat"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -271,6 +290,7 @@ function onClose() {
:placeholder="t('notification.telegram.usersPlaceholder')" :placeholder="t('notification.telegram.usersPlaceholder')"
:hint="t('notification.telegram.usersHint')" :hint="t('notification.telegram.usersHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-account-group"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -280,6 +300,7 @@ function onClose() {
:placeholder="t('notification.telegram.adminsPlaceholder')" :placeholder="t('notification.telegram.adminsPlaceholder')"
:hint="t('notification.telegram.adminsHint')" :hint="t('notification.telegram.adminsHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-account-supervisor"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -291,6 +312,7 @@ function onClose() {
:placeholder="t('notification.name')" :placeholder="t('notification.name')"
:hint="t('notification.nameHint')" :hint="t('notification.nameHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-label"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -300,6 +322,7 @@ function onClose() {
:placeholder="t('notification.slack.oauthTokenPlaceholder')" :placeholder="t('notification.slack.oauthTokenPlaceholder')"
:hint="t('notification.slack.oauthTokenHint')" :hint="t('notification.slack.oauthTokenHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-key"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -309,6 +332,7 @@ function onClose() {
:placeholder="t('notification.slack.appTokenPlaceholder')" :placeholder="t('notification.slack.appTokenPlaceholder')"
:hint="t('notification.slack.appTokenHint')" :hint="t('notification.slack.appTokenHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-application"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -318,6 +342,7 @@ function onClose() {
:placeholder="t('notification.slack.channelPlaceholder')" :placeholder="t('notification.slack.channelPlaceholder')"
:hint="t('notification.slack.channelHint')" :hint="t('notification.slack.channelHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-pound"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -329,6 +354,7 @@ function onClose() {
:placeholder="t('notification.name')" :placeholder="t('notification.name')"
:hint="t('notification.nameHint')" :hint="t('notification.nameHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-label"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -337,6 +363,7 @@ function onClose() {
:label="t('notification.synologychat.webhook')" :label="t('notification.synologychat.webhook')"
:hint="t('notification.synologychat.webhookHint')" :hint="t('notification.synologychat.webhookHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-webhook"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -345,6 +372,7 @@ function onClose() {
:label="t('notification.synologychat.token')" :label="t('notification.synologychat.token')"
:hint="t('notification.synologychat.tokenHint')" :hint="t('notification.synologychat.tokenHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-key"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -356,6 +384,7 @@ function onClose() {
:placeholder="t('notification.name')" :placeholder="t('notification.name')"
:hint="t('notification.nameHint')" :hint="t('notification.nameHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-label"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -364,6 +393,7 @@ function onClose() {
:label="t('notification.vocechat.host')" :label="t('notification.vocechat.host')"
:hint="t('notification.vocechat.hostHint')" :hint="t('notification.vocechat.hostHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-server"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -372,6 +402,7 @@ function onClose() {
:label="t('notification.vocechat.apiKey')" :label="t('notification.vocechat.apiKey')"
:hint="t('notification.vocechat.apiKeyHint')" :hint="t('notification.vocechat.apiKeyHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-key"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -381,6 +412,7 @@ function onClose() {
:placeholder="t('notification.vocechat.channelIdPlaceholder')" :placeholder="t('notification.vocechat.channelIdPlaceholder')"
:hint="t('notification.vocechat.channelIdHint')" :hint="t('notification.vocechat.channelIdHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-pound"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -392,6 +424,7 @@ function onClose() {
:placeholder="t('notification.name')" :placeholder="t('notification.name')"
:hint="t('notification.nameHint')" :hint="t('notification.nameHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-label"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -400,6 +433,7 @@ function onClose() {
:label="t('notification.webpush.username')" :label="t('notification.webpush.username')"
:hint="t('notification.webpush.usernameHint')" :hint="t('notification.webpush.usernameHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-account"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -411,6 +445,7 @@ function onClose() {
:hint="t('notification.customTypeHint')" :hint="t('notification.customTypeHint')"
persistent-hint persistent-hint
active active
prepend-inner-icon="mdi-cog"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -419,7 +454,7 @@ function onClose() {
:label="t('notification.name')" :label="t('notification.name')"
:hint="t('notification.nameRequired')" :hint="t('notification.nameRequired')"
persistent-hint persistent-hint
active prepend-inner-icon="mdi-label"
/> />
</VCol> </VCol>
</VRow> </VRow>

View File

@@ -229,7 +229,7 @@ const dropdownItems = ref([
class="flex flex-col align-self-baseline justify-between px-2 py-2 w-full overflow-hidden max-h-10 min-h-10" class="flex flex-col align-self-baseline justify-between px-2 py-2 w-full overflow-hidden max-h-10 min-h-10"
> >
<div class="flex flex-nowrap items-center w-full pe-10"> <div class="flex flex-nowrap items-center w-full pe-10">
<div class="flex flex-nowrap max-w-32 items-center align-middle"> <div class="flex flex-nowrap max-w-40 items-center align-middle">
<VIcon icon="mdi-github" class="me-1" /> <VIcon icon="mdi-github" class="me-1" />
<a <a
class="overflow-hidden text-ellipsis whitespace-nowrap" class="overflow-hidden text-ellipsis whitespace-nowrap"

View File

@@ -10,6 +10,7 @@ import VersionHistory from '@/components/misc/VersionHistory.vue'
import ProgressDialog from '../dialog/ProgressDialog.vue' import ProgressDialog from '../dialog/ProgressDialog.vue'
import PluginConfigDialog from '../dialog/PluginConfigDialog.vue' import PluginConfigDialog from '../dialog/PluginConfigDialog.vue'
import PluginDataDialog from '../dialog/PluginDataDialog.vue' import PluginDataDialog from '../dialog/PluginDataDialog.vue'
import LoggingView from '@/views/system/LoggingView.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
@@ -58,6 +59,9 @@ const progressDialog = ref(false)
// 插件数据页面 // 插件数据页面
const pluginInfoDialog = ref(false) const pluginInfoDialog = ref(false)
// 实时日志弹窗
const loggingDialog = ref(false)
// 进度框文本 // 进度框文本
const progressText = ref('正在更新插件...') const progressText = ref('正在更新插件...')
@@ -73,6 +77,18 @@ const imageLoadError = ref(false)
// 更新日志弹窗 // 更新日志弹窗
const releaseDialog = ref(false) const releaseDialog = ref(false)
// 插件分身对话框
const pluginCloneDialog = ref(false)
// 插件分身表单
const cloneForm = ref({
suffix: '',
name: '',
description: '',
version: '',
icon: '',
})
// 监听动作标识如为true则打开详情 // 监听动作标识如为true则打开详情
watch( watch(
() => props.action, () => props.action,
@@ -124,7 +140,12 @@ async function uninstallPlugin() {
// 通知父组件刷新 // 通知父组件刷新
emit('remove') emit('remove')
} else { } else {
$toast.error(t('plugin.uninstallFailed', { name: props.plugin?.plugin_name, message: result.message })) $toast.error(
t('plugin.uninstallFailed', {
name: props.plugin?.plugin_name,
message: result.message,
}),
)
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -178,7 +199,12 @@ async function resetPlugin() {
// 通知父组件刷新 // 通知父组件刷新
emit('save') emit('save')
} else { } else {
$toast.error(t('plugin.resetFailed', { name: props.plugin?.plugin_name, message: result.message })) $toast.error(
t('plugin.resetFailed', {
name: props.plugin?.plugin_name,
message: result.message,
}),
)
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -209,7 +235,12 @@ async function updatePlugin() {
// 通知父组件刷新 // 通知父组件刷新
emit('save') emit('save')
} else { } else {
$toast.error(t('plugin.updateFailed', { name: props.plugin?.plugin_name, message: result.message })) $toast.error(
t('plugin.updateFailed', {
name: props.plugin?.plugin_name,
message: result.message,
}),
)
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -241,6 +272,54 @@ function configDone() {
emit('save') emit('save')
} }
// 显示插件分身对话框
function showPluginClone() {
cloneForm.value = {
suffix: '',
name: t('plugin.cloneDefaultName', { name: props.plugin?.plugin_name }),
description: t('plugin.cloneDefaultDescription', { description: props.plugin?.plugin_desc }),
version: props.plugin?.plugin_version || '1.0',
icon: props.plugin?.plugin_icon || '',
}
pluginCloneDialog.value = true
}
// 执行插件分身
async function executePluginClone() {
if (!cloneForm.value.suffix.trim()) {
$toast.error(t('plugin.suffixRequired'))
return
}
try {
progressDialog.value = true
progressText.value = t('plugin.cloning', { name: props.plugin?.plugin_name })
const result: { [key: string]: any } = await api.post(`plugin/clone/${props.plugin?.id}`, {
suffix: cloneForm.value.suffix.trim(),
name: cloneForm.value.name.trim(),
description: cloneForm.value.description.trim(),
version: cloneForm.value.version.trim(),
icon: cloneForm.value.icon.trim(),
})
progressDialog.value = false
if (result.success) {
$toast.success(t('plugin.cloneSuccess', { name: cloneForm.value.name }))
pluginCloneDialog.value = false
// 通知父组件刷新
emit('remove')
} else {
$toast.error(t('plugin.cloneFailed', { message: result.message }))
}
} catch (error) {
progressDialog.value = false
$toast.error(t('plugin.cloneFailedGeneral'))
console.error(error)
}
}
// 弹出菜单 // 弹出菜单
const dropdownItems = ref([ const dropdownItems = ref([
{ {
@@ -261,6 +340,16 @@ const dropdownItems = ref([
click: showPluginConfig, click: showPluginConfig,
}, },
}, },
{
title: t('plugin.clone'),
value: 8,
show: true,
props: {
prependIcon: 'mdi-content-copy',
color: 'info',
click: showPluginClone,
},
},
{ {
title: t('plugin.update'), title: t('plugin.update'),
value: 3, value: 3,
@@ -298,7 +387,7 @@ const dropdownItems = ref([
props: { props: {
prependIcon: 'mdi-file-document-outline', prependIcon: 'mdi-file-document-outline',
click: () => { click: () => {
openLoggerWindow() loggingDialog.value = true
}, },
}, },
}, },
@@ -366,7 +455,7 @@ watch(
{{ props.plugin?.plugin_desc }} {{ props.plugin?.plugin_desc }}
</div> </div>
</div> </div>
<div class="relative flex-shrink-0 self-center cursor-move pb-3"> <div class="relative flex-shrink-0 self-center pb-3" :class="{ 'cursor-move': display.mdAndUp.value }">
<VAvatar size="48"> <VAvatar size="48">
<VImg <VImg
ref="imageRef" ref="imageRef"
@@ -384,7 +473,7 @@ watch(
class="flex flex-col align-self-baseline justify-between px-2 py-2 w-full overflow-hidden max-h-10 min-h-10" class="flex flex-col align-self-baseline justify-between px-2 py-2 w-full overflow-hidden max-h-10 min-h-10"
> >
<div class="flex flex-nowrap items-center w-full pe-10"> <div class="flex flex-nowrap items-center w-full pe-10">
<div class="flex flex-nowrap max-w-32 items-center align-middle"> <div class="flex flex-nowrap max-w-40 items-center align-middle">
<VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true"> <VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true">
<VIcon v-if="!isAvatarLoaded" size="small" icon="mdi-github" class="me-1" /> <VIcon v-if="!isAvatarLoaded" size="small" icon="mdi-github" class="me-1" />
</VImg> </VImg>
@@ -470,6 +559,144 @@ watch(
</VCardItem> </VCardItem>
</VCard> </VCard>
</VDialog> </VDialog>
<!-- 实时日志弹窗 -->
<VDialog
v-if="loggingDialog"
v-model="loggingDialog"
scrollable
max-width="60rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VDialogCloseBtn @click="loggingDialog = false" />
<VCardItem>
<VCardTitle class="d-inline-flex">
<VIcon icon="mdi-file-document" class="me-2" />
{{ t('plugin.logTitle') }}
<a class="mx-2 d-inline-flex align-center cursor-pointer" @click="openLoggerWindow">
<VChip color="grey-darken-1" size="small" class="ml-2">
<VIcon icon="mdi-open-in-new" size="small" start />
{{ t('common.openInNewWindow') }}
</VChip>
</a>
</VCardTitle>
</VCardItem>
<VDivider />
<VCardText>
<LoggingView :logfile="`plugins/${props.plugin?.id?.toLowerCase()}.log`" />
</VCardText>
</VCard>
</VDialog>
<!-- 插件分身对话框 -->
<VDialog
v-if="pluginCloneDialog"
v-model="pluginCloneDialog"
width="600"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-content-copy" class="me-2" />
</template>
<VCardTitle>{{ t('plugin.cloneTitle') }}</VCardTitle>
<VCardSubtitle>{{ t('plugin.cloneSubtitle', { name: props.plugin?.plugin_name }) }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="pluginCloneDialog = false" />
<VDivider />
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="cloneForm.suffix"
:label="t('plugin.suffix') + ' *'"
:placeholder="t('plugin.suffixPlaceholder')"
:hint="t('plugin.suffixHint')"
persistent-hint
:rules="[
v => !!v || t('plugin.suffixRequired'),
v => /^[a-zA-Z0-9]+$/.test(v) || t('plugin.suffixFormatError'),
v => v.length <= 20 || t('plugin.suffixLengthError'),
]"
required
prepend-inner-icon="mdi-tag"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="cloneForm.name"
:label="t('plugin.cloneName')"
:placeholder="t('plugin.cloneNamePlaceholder')"
:hint="t('plugin.cloneNameHint')"
persistent-hint
prepend-inner-icon="mdi-rename-box"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="cloneForm.description"
:label="t('plugin.cloneDescriptionLabel')"
:placeholder="t('plugin.cloneDescriptionPlaceholder')"
:hint="t('plugin.cloneDescriptionHint')"
persistent-hint
prepend-inner-icon="mdi-text"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="cloneForm.version"
:label="t('plugin.cloneVersion')"
:placeholder="t('plugin.cloneVersionPlaceholder')"
:hint="t('plugin.cloneVersionHint')"
persistent-hint
prepend-inner-icon="mdi-numeric"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="cloneForm.icon"
:label="t('plugin.cloneIcon')"
:placeholder="t('plugin.cloneIconPlaceholder')"
:hint="t('plugin.cloneIconHint')"
persistent-hint
prepend-inner-icon="mdi-image"
/>
</VCol>
<!-- 重要提醒 -->
<VCol cols="12">
<VAlert type="warning" variant="tonal" density="compact" class="mt-2" icon="mdi-alert-circle-outline">
<div class="text-body-2">
<strong>{{ t('common.notice') }}</strong
>{{ t('plugin.cloneNotice') }}
</div>
</VAlert>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions class="pt-3">
<VSpacer />
<VBtn
color="primary"
@click="executePluginClone"
prepend-icon="mdi-content-copy"
class="px-5"
:disabled="!cloneForm.suffix.trim()"
>
{{ t('plugin.createClone') }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</div> </div>
</template> </template>

View File

@@ -298,13 +298,18 @@ const dropdownItems = ref([
<div class="plugin-folder-card__body" :class="{ 'plugin-folder-card__body--no-icon': !shouldShowIcon }"> <div class="plugin-folder-card__body" :class="{ 'plugin-folder-card__body--no-icon': !shouldShowIcon }">
<!-- 文件夹图标 --> <!-- 文件夹图标 -->
<div v-if="shouldShowIcon" class="plugin-folder-card__icon-container"> <div v-if="shouldShowIcon" class="plugin-folder-card__icon-container">
<VIcon :icon="folderIcon" :size="display.mobile ? 56 : 72" class="cursor-move" :color="iconColor" /> <VIcon
:icon="folderIcon"
:size="display.mobile ? 56 : 72"
:color="iconColor"
:class="{ 'cursor-move': display.mdAndUp.value }"
/>
</div> </div>
<!-- 文件夹信息 --> <!-- 文件夹信息 -->
<div <div
class="plugin-folder-card__info cursor-move" class="plugin-folder-card__info"
:class="{ 'plugin-folder-card__info--no-icon': !shouldShowIcon }" :class="{ 'cursor-move': display.mdAndUp.value, 'plugin-folder-card__info--no-icon': !shouldShowIcon }"
> >
<!-- 文件夹名称 --> <!-- 文件夹名称 -->
<h3 class="plugin-folder-card__name"> <h3 class="plugin-folder-card__name">
@@ -347,10 +352,13 @@ const dropdownItems = ref([
<!-- 重命名对话框 --> <!-- 重命名对话框 -->
<VDialog v-if="renameDialog" v-model="renameDialog" max-width="400"> <VDialog v-if="renameDialog" v-model="renameDialog" max-width="400">
<VCard> <VCard>
<VDialogCloseBtn @click="renameDialog = false" />
<VCardItem> <VCardItem>
<template #prepend>
<VIcon icon="mdi-pencil" class="me-2" />
</template>
<VCardTitle>{{ t('folder.renameFolder') }}</VCardTitle> <VCardTitle>{{ t('folder.renameFolder') }}</VCardTitle>
</VCardItem> </VCardItem>
<VDialogCloseBtn @click="renameDialog = false" />
<VDivider /> <VDivider />
<VCardText> <VCardText>
<VTextField <VTextField
@@ -462,6 +470,7 @@ const dropdownItems = ref([
variant="outlined" variant="outlined"
:hint="t('folder.customBackgroundImageHint')" :hint="t('folder.customBackgroundImageHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-image"
/> />
</VCol> </VCol>
</VRow> </VRow>

View File

@@ -0,0 +1,183 @@
<script lang="ts" setup>
import PluginCard from './PluginCard.vue'
import PluginFolderCard from './PluginFolderCard.vue'
interface MixedSortItem {
type: 'folder' | 'plugin'
id: string
data: any
order: number
}
interface Props {
item: MixedSortItem
pluginStatistics?: { [key: string]: number }
pluginActions?: { [key: string]: boolean }
showRemoveButton?: boolean
}
const props = withDefaults(defineProps<Props>(), {
pluginStatistics: () => ({}),
pluginActions: () => ({}),
showRemoveButton: false,
})
const emit = defineEmits<{
openFolder: [folderName: string]
deleteFolder: [folderName: string]
renameFolder: [oldName: string, newName: string]
updateFolderConfig: [folderName: string, config: any]
refreshData: []
actionDone: [pluginId: string]
removeFromFolder: [pluginId: string]
dropToFolder: [event: DragEvent, folderName: string]
}>()
// 拖拽事件处理
function handleDragOver(event: DragEvent) {
// 只有当拖拽的是插件时才允许放入文件夹
if (props.item.type === 'folder') {
event.preventDefault()
event.stopPropagation()
event.dataTransfer!.dropEffect = 'move'
const target = event.currentTarget as HTMLElement
target.classList.add('drag-over')
}
}
function handleDragEnter(event: DragEvent) {
if (props.item.type === 'folder') {
event.preventDefault()
event.stopPropagation()
}
}
function handleDragLeave(event: DragEvent) {
if (props.item.type === 'folder') {
event.preventDefault()
event.stopPropagation()
const target = event.currentTarget as HTMLElement
target.classList.remove('drag-over')
}
}
function handleDropToFolder(event: DragEvent) {
if (props.item.type === 'folder') {
event.preventDefault()
event.stopPropagation()
const target = event.currentTarget as HTMLElement
target.classList.remove('drag-over')
emit('dropToFolder', event, props.item.id)
}
}
</script>
<template>
<div class="mixed-sort-card-wrapper h-full">
<!-- 文件夹卡片 -->
<div
v-if="item.type === 'folder'"
class="drop-zone h-full"
:data-plugin-id="item.id"
@dragover="handleDragOver"
@dragenter="handleDragEnter"
@dragleave="handleDragLeave"
@drop="handleDropToFolder"
>
<PluginFolderCard
:folder-name="item.data.name"
:plugin-count="item.data.pluginCount"
:folder-config="item.data.config"
@open="$emit('openFolder', item.id)"
@delete="$emit('deleteFolder', item.id)"
@rename="(oldName, newName) => $emit('renameFolder', oldName, newName)"
@update-config="(folderName, config) => $emit('updateFolderConfig', folderName, config)"
/>
</div>
<!-- 插件卡片 -->
<div v-else-if="item.type === 'plugin'" class="plugin-item-wrapper h-full" :data-plugin-id="item.id">
<PluginCard
:count="pluginStatistics[item.id] || 0"
:plugin="item.data"
:action="pluginActions[item.id] || false"
@remove="$emit('refreshData')"
@save="$emit('refreshData')"
@action-done="$emit('actionDone', item.id)"
/>
<!-- 移出文件夹按钮(仅在文件夹内显示) -->
<VBtn
v-if="showRemoveButton"
icon="mdi-folder-remove"
variant="text"
color="warning"
size="small"
class="remove-from-folder-btn"
@click="$emit('removeFromFolder', item.id)"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.mixed-sort-card-wrapper {
block-size: 100%;
inline-size: 100%;
// 确保拖拽时的边界清晰
&.sortable-chosen {
opacity: 0.5;
}
&.sortable-ghost {
border: 2px dashed #2196f3;
border-radius: 16px;
background: rgba(33, 150, 243, 10%);
opacity: 0.3;
}
}
// 拖拽相关样式
.drop-zone {
position: relative;
isolation: isolate; // 创建新的层叠上下文
transition: all 0.3s ease;
&.drag-over {
border: 2px dashed #2196f3;
border-radius: 16px;
box-shadow: 0 0 20px rgba(33, 150, 243, 50%);
transform: scale(1.02);
}
}
.plugin-item-wrapper {
position: relative;
isolation: isolate; // 创建新的层叠上下文
.remove-from-folder-btn {
position: absolute;
z-index: 10;
border-radius: 50%;
backdrop-filter: blur(4px);
background: rgba(255, 255, 255, 10%);
inset-block-start: 4px;
inset-inline-end: 4px;
opacity: 0;
transition: opacity 0.3s ease;
}
&:hover .remove-from-folder-btn {
opacity: 1;
}
}
// 拖拽时的样式优化
.mixed-sort-card-wrapper.sortable-drag {
.remove-from-folder-btn {
display: none !important;
}
}
</style>

View File

@@ -213,6 +213,9 @@ function onClose() {
> >
<VCard> <VCard>
<VCardItem> <VCardItem>
<template #prepend>
<VIcon icon="mdi-cog" />
</template>
<VCardTitle>{{ t('storage.custom') }}</VCardTitle> <VCardTitle>{{ t('storage.custom') }}</VCardTitle>
<VDialogCloseBtn v-model="customConfigDialog" /> <VDialogCloseBtn v-model="customConfigDialog" />
</VCardItem> </VCardItem>
@@ -225,11 +228,16 @@ function onClose() {
:label="t('storage.type')" :label="t('storage.type')"
:hint="t('storage.customTypeHint')" :hint="t('storage.customTypeHint')"
persistent-hint persistent-hint
active prepend-inner-icon="mdi-database"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField v-model="customName" :label="t('storage.name')" persistent-hint active /> <VTextField
v-model="customName"
:label="t('storage.name')"
persistent-hint
prepend-inner-icon="mdi-label"
/>
</VCol> </VCol>
</VRow> </VRow>
</VCardText> </VCardText>

View File

@@ -353,7 +353,7 @@ function onSubscribeEditRemove() {
<div> <div>
<VCardText class="flex items-center pt-3 pb-2"> <VCardText class="flex items-center pt-3 pb-2">
<div <div
class="h-auto w-14 flex-shrink-0 overflow-hidden rounded-md" class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md"
v-if="imageLoaded" v-if="imageLoaded"
:class="{ 'cursor-move': display.mdAndUp.value }" :class="{ 'cursor-move': display.mdAndUp.value }"
> >
@@ -367,7 +367,7 @@ function onSubscribeEditRemove() {
</div> </div>
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4"> <div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div> <div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
<div class="mr-2 min-w-0 text-lg font-bold text-white"> <div class="mr-2 min-w-0 text-lg font-bold text-white text-ellipsis overflow-hidden line-clamp-2 ...">
{{ props.media?.name }} {{ props.media?.name }}
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }} {{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
</div> </div>

View File

@@ -179,7 +179,13 @@ const resolveProgress = (item: Workflow) => {
:loading="loading" :loading="loading"
:class="{ 'transition transform-cpu duration-300 -translate-y-1': hover.isHovering }" :class="{ 'transition transform-cpu duration-300 -translate-y-1': hover.isHovering }"
> >
<VCardItem class="py-3" :class="`bg-${resolveStatusVariant(workflow?.state).color}`"> <VCardItem
:class="{
'py-1': workflow?.description,
'py-3': !workflow?.description,
[`bg-${resolveStatusVariant(workflow?.state).color}`]: true,
}"
>
<template #prepend> <template #prepend>
<VAvatar variant="text" class="me-2"> <VAvatar variant="text" class="me-2">
<VIcon <VIcon

View File

@@ -6,10 +6,6 @@ import type { DownloaderConf, MediaInfo, TorrentInfo, TransferDirectoryConf } fr
import { formatFileSize } from '@/@core/utils/formatters' import { formatFileSize } from '@/@core/utils/formatters'
import { VCardTitle, VChip } from 'vuetify/lib/components/index.mjs' import { VCardTitle, VChip } from 'vuetify/lib/components/index.mjs'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 多语言支持 // 多语言支持
const { t } = useI18n() const { t } = useI18n()
@@ -136,71 +132,77 @@ onMounted(() => {
}) })
</script> </script>
<template> <template>
<VDialog max-width="35rem" scrollable :fullscreen="!display.mdAndUp.value"> <VDialog max-width="35rem" scrollable>
<VCard> <VCard>
<VCardTitle class="py-4 me-12"> <VCardItem class="py-2">
<VIcon icon="mdi-download" class="me-2" /> <template #prepend>
<span v-if="title">{{ torrent?.site_name }} - {{ title }}</span> <VIcon icon="mdi-monitor-arrow-down-variant" class="me-2" />
<span v-else>{{ t('dialog.addDownload.confirmDownload') }}</span> </template>
</VCardTitle> <VCardTitle>{{ t('dialog.addDownload.confirmDownload') }}</VCardTitle>
<VCardSubtitle>{{ torrent?.site_name }} - {{ title }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" /> <VDialogCloseBtn @click="emit('close')" />
<VDivider /> <VDivider />
<VList lines="one"> <VCardText>
<VListItem> <VList lines="one">
<template #prepend> <VListItem>
<VIcon icon="mdi-web"></VIcon> <template #prepend>
</template> <VIcon icon="mdi-web"></VIcon>
<VListItemTitle> </template>
<span class="whitespace-break-spaces me-2">{{ torrent?.title }}</span> <VListItemTitle>
<span class="text-green-700 ms-2 text-sm">{{ torrent?.seeders }}</span> <span class="whitespace-break-spaces me-2">{{ torrent?.title }}</span>
<span class="text-orange-700 ms-2 text-sm">{{ torrent?.peers }}</span> <span class="text-green-700 ms-2 text-sm">{{ torrent?.seeders }}</span>
</VListItemTitle> <span class="text-orange-700 ms-2 text-sm">{{ torrent?.peers }}</span>
</VListItem> </VListItemTitle>
<VListItem v-if="torrent?.description"> </VListItem>
<template #prepend> <VListItem v-if="torrent?.description">
<VIcon icon="mdi-subtitles-outline"></VIcon> <template #prepend>
</template> <VIcon icon="mdi-subtitles-outline"></VIcon>
<VListItemTitle> </template>
<span class="text-body-2 whitespace-break-spaces">{{ torrent?.description }}</span> <VListItemTitle>
</VListItemTitle> <span class="text-body-2 whitespace-break-spaces">{{ torrent?.description }}</span>
</VListItem> </VListItemTitle>
<VListItem v-if="torrent?.size"> </VListItem>
<template #prepend> <VListItem v-if="torrent?.size">
<VIcon icon="mdi-database"></VIcon> <template #prepend>
</template> <VIcon icon="mdi-database"></VIcon>
<VListItemTitle> </template>
<span class="text-body-2"> <VListItemTitle>
<VChip variant="tonal" label> <span class="text-body-2">
{{ formatFileSize(torrent?.size || 0) }} <VChip variant="tonal" label>
</VChip> {{ formatFileSize(torrent?.size || 0) }}
</span> </VChip>
</VListItemTitle> </span>
</VListItem> </VListItemTitle>
</VList> </VListItem>
<VRow class="px-7"> </VList>
<VCol cols="12" md="4"> <VRow class="px-5">
<VSelect <VCol cols="12" md="6">
v-model="selectedDownloader" <VSelect
:items="downloaderOptions" v-model="selectedDownloader"
size="small" :items="downloaderOptions"
:label="t('dialog.addDownload.downloader')" size="small"
variant="underlined" :label="t('dialog.addDownload.downloader')"
:placeholder="t('dialog.addDownload.defaultPlaceholder')" variant="underlined"
density="compact" :placeholder="t('dialog.addDownload.defaultPlaceholder')"
/> density="comfortable"
</VCol> prepend-inner-icon="mdi-download"
<VCol cols="12" md="8"> />
<VCombobox </VCol>
v-model="selectedDirectory" <VCol cols="12" md="6">
:items="targetDirectories" <VCombobox
:label="t('dialog.addDownload.saveDirectory')" v-model="selectedDirectory"
size="small" :items="targetDirectories"
:placeholder="t('dialog.addDownload.autoPlaceholder')" :label="t('dialog.addDownload.saveDirectory')"
variant="underlined" size="small"
density="compact" :placeholder="t('dialog.addDownload.autoPlaceholder')"
/> variant="underlined"
</VCol> density="comfortable"
</VRow> prepend-inner-icon="mdi-folder"
/>
</VCol>
</VRow>
</VCardText>
<VCardText class="text-center"> <VCardText class="text-center">
<VBtn variant="elevated" :disabled="loading" @click="addDownload" :prepend-icon="icon" class="px-5"> <VBtn variant="elevated" :disabled="loading" @click="addDownload" :prepend-icon="icon" class="px-5">
{{ buttonText }} {{ buttonText }}

View File

@@ -74,6 +74,9 @@ async function savaAlistConfig() {
<VCard> <VCard>
<VDialogCloseBtn @click="emit('close')" /> <VDialogCloseBtn @click="emit('close')" />
<VCardItem> <VCardItem>
<template #prepend>
<VIcon icon="mdi-cog-outline" class="me-2" />
</template>
<VCardTitle> <VCardTitle>
{{ t('dialog.alistConfig.title') }} {{ t('dialog.alistConfig.title') }}
</VCardTitle> </VCardTitle>
@@ -87,6 +90,7 @@ async function savaAlistConfig() {
:hint="t('dialog.alistConfig.serverUrl')" :hint="t('dialog.alistConfig.serverUrl')"
:label="t('dialog.alistConfig.serverUrl')" :label="t('dialog.alistConfig.serverUrl')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-server"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -96,6 +100,7 @@ async function savaAlistConfig() {
:label="t('dialog.alistConfig.loginType')" :label="t('dialog.alistConfig.loginType')"
:hint="t('dialog.alistConfig.loginType')" :hint="t('dialog.alistConfig.loginType')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-login"
/> />
</VCol> </VCol>
<VCol cols="12" md="4" v-if="loginType == 'username'"> <VCol cols="12" md="4" v-if="loginType == 'username'">
@@ -104,6 +109,7 @@ async function savaAlistConfig() {
:hint="t('dialog.alistConfig.username')" :hint="t('dialog.alistConfig.username')"
:label="t('dialog.alistConfig.username')" :label="t('dialog.alistConfig.username')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-account"
/> />
</VCol> </VCol>
<VCol cols="12" md="4" v-if="loginType == 'username'"> <VCol cols="12" md="4" v-if="loginType == 'username'">
@@ -113,6 +119,7 @@ async function savaAlistConfig() {
:hint="t('dialog.alistConfig.password')" :hint="t('dialog.alistConfig.password')"
:label="t('dialog.alistConfig.password')" :label="t('dialog.alistConfig.password')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-lock"
/> />
</VCol> </VCol>
<VCol cols="12" md="8" v-if="loginType == 'token'"> <VCol cols="12" md="8" v-if="loginType == 'token'">
@@ -121,6 +128,7 @@ async function savaAlistConfig() {
:hint="t('dialog.alistConfig.loginTypeOptions.token')" :hint="t('dialog.alistConfig.loginTypeOptions.token')"
:label="t('dialog.alistConfig.loginTypeOptions.token')" :label="t('dialog.alistConfig.loginTypeOptions.token')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-key"
/> />
</VCol> </VCol>
</VRow> </VRow>

View File

@@ -114,6 +114,9 @@ onUnmounted(() => {
<VCard> <VCard>
<VDialogCloseBtn @click="emit('close')" /> <VDialogCloseBtn @click="emit('close')" />
<VCardItem> <VCardItem>
<template #prepend>
<VIcon icon="mdi-qrcode" class="me-2" />
</template>
<VCardTitle> <VCardTitle>
{{ t('dialog.aliyunAuth.loginTitle') }} {{ t('dialog.aliyunAuth.loginTitle') }}
</VCardTitle> </VCardTitle>

View File

@@ -25,10 +25,16 @@ function handleImport() {
<template> <template>
<VDialog width="40rem" scrollable max-height="85vh"> <VDialog width="40rem" scrollable max-height="85vh">
<VCard :title="props.title"> <VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-code-json" class="me-2" />
</template>
<VCardTitle>{{ props.title }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" /> <VDialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2"> <VCardText class="pt-2">
<VTextarea v-model="codeString" /> <VTextarea v-model="codeString" prepend-inner-icon="mdi-code-json" />
</VCardText> </VCardText>
<VCardActions> <VCardActions>
<VSpacer /> <VSpacer />

View File

@@ -61,6 +61,9 @@ async function handleReset() {
<VCard> <VCard>
<VDialogCloseBtn @click="emit('close')" /> <VDialogCloseBtn @click="emit('close')" />
<VCardItem> <VCardItem>
<template #prepend>
<VIcon icon="mdi-cog-outline" class="me-2" />
</template>
<VCardTitle> <VCardTitle>
{{ t('dialog.rcloneConfig.title') }} {{ t('dialog.rcloneConfig.title') }}
</VCardTitle> </VCardTitle>
@@ -69,7 +72,11 @@ async function handleReset() {
<VCardText> <VCardText>
<VRow> <VRow>
<VCol cols="12"> <VCol cols="12">
<VTextField v-model="props.conf.filepath" :label="t('dialog.rcloneConfig.filePath')" /> <VTextField
v-model="props.conf.filepath"
:label="t('dialog.rcloneConfig.filePath')"
prepend-inner-icon="mdi-file-document"
/>
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
<VAceEditor <VAceEditor

View File

@@ -82,15 +82,18 @@ const storageOptions = computed(() => {
// 标题 // 标题
const dialogTitle = computed(() => { const dialogTitle = computed(() => {
return t('dialog.reorganize.manualTitle')
})
// 副标题
const dialogSubtitle = computed(() => {
if (props.items) { if (props.items) {
if (props.items.length > 1) return t('dialog.reorganize.multipleItemsTitle', { count: props.items.length }) if (props.items.length > 1) return t('dialog.reorganize.multipleItemsTitle', { count: props.items.length })
return t('dialog.reorganize.singleItemTitle', { path: props.items[0].path }) return t('dialog.reorganize.singleItemTitle', { path: props.items[0].path })
} else if (props.logids) { } else if (props.logids) {
return t('dialog.reorganize.multipleItemsTitle', { count: props.logids.length }) return t('dialog.reorganize.multipleItemsTitle', { count: props.logids.length })
} }
return t('dialog.reorganize.manualTitle')
}) })
// 禁用指定集数 // 禁用指定集数
const disableEpisodeDetail = computed(() => { const disableEpisodeDetail = computed(() => {
if (props.items) { if (props.items) {
@@ -250,7 +253,12 @@ onUnmounted(() => {
<template> <template>
<VDialog scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value"> <VDialog scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="dialogTitle"> <VCard>
<VCardItem class="py-2">
<template #prepend> <VIcon icon="mdi-folder-move" class="me-2" /> </template>
<VCardTitle>{{ dialogTitle }}</VCardTitle>
<VCardSubtitle>{{ dialogSubtitle }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" /> <VDialogCloseBtn @click="emit('close')" />
<VDivider /> <VDivider />
<VCardText> <VCardText>
@@ -264,6 +272,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.targetPathPlaceholder')" :placeholder="t('dialog.reorganize.targetPathPlaceholder')"
:hint="t('dialog.reorganize.targetStorageHint')" :hint="t('dialog.reorganize.targetStorageHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-harddisk"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -273,6 +282,7 @@ onUnmounted(() => {
:items="transferTypeOptions" :items="transferTypeOptions"
:hint="t('dialog.reorganize.transferTypeHint')" :hint="t('dialog.reorganize.transferTypeHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-swap-horizontal"
> >
<template v-slot:selection="{ item }"> <template v-slot:selection="{ item }">
{{ transferForm.transfer_type === '' ? t('dialog.reorganize.auto') : item.title }} {{ transferForm.transfer_type === '' ? t('dialog.reorganize.auto') : item.title }}
@@ -287,6 +297,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.targetPathPlaceholder')" :placeholder="t('dialog.reorganize.targetPathPlaceholder')"
:hint="t('dialog.reorganize.targetPathHint')" :hint="t('dialog.reorganize.targetPathHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-folder-outline"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -302,6 +313,7 @@ onUnmounted(() => {
]" ]"
:hint="t('dialog.reorganize.mediaTypeHint')" :hint="t('dialog.reorganize.mediaTypeHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-movie-open"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -315,6 +327,7 @@ onUnmounted(() => {
append-inner-icon="mdi-magnify" append-inner-icon="mdi-magnify"
:hint="t('dialog.reorganize.mediaIdHint')" :hint="t('dialog.reorganize.mediaIdHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-identifier"
@click:append-inner="mediaSelectorDialog = true" @click:append-inner="mediaSelectorDialog = true"
/> />
<VTextField <VTextField
@@ -327,6 +340,7 @@ onUnmounted(() => {
append-inner-icon="mdi-magnify" append-inner-icon="mdi-magnify"
:hint="t('dialog.reorganize.mediaIdHint')" :hint="t('dialog.reorganize.mediaIdHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-identifier"
@click:append-inner="mediaSelectorDialog = true" @click:append-inner="mediaSelectorDialog = true"
/> />
</VCol> </VCol>
@@ -339,6 +353,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.episodeGroupPlaceholder')" :placeholder="t('dialog.reorganize.episodeGroupPlaceholder')"
:hint="t('dialog.reorganize.episodeGroupHint')" :hint="t('dialog.reorganize.episodeGroupHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-view-list"
/> />
</VCol> </VCol>
<VCol cols="12" md="3"> <VCol cols="12" md="3">
@@ -348,6 +363,7 @@ onUnmounted(() => {
:items="seasonItems" :items="seasonItems"
:hint="t('dialog.reorganize.seasonHint')" :hint="t('dialog.reorganize.seasonHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-calendar"
/> />
</VCol> </VCol>
<VCol cols="12" md="3"> <VCol cols="12" md="3">
@@ -358,6 +374,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.episodeDetailPlaceholder')" :placeholder="t('dialog.reorganize.episodeDetailPlaceholder')"
:hint="t('dialog.reorganize.episodeDetailHint')" :hint="t('dialog.reorganize.episodeDetailHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-playlist-play"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -367,6 +384,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.episodeFormatPlaceholder')" :placeholder="t('dialog.reorganize.episodeFormatPlaceholder')"
:hint="t('dialog.reorganize.episodeFormatHint')" :hint="t('dialog.reorganize.episodeFormatHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-format-text"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -376,6 +394,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.episodeOffsetPlaceholder')" :placeholder="t('dialog.reorganize.episodeOffsetPlaceholder')"
:hint="t('dialog.reorganize.episodeOffsetHint')" :hint="t('dialog.reorganize.episodeOffsetHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-numeric"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -387,6 +406,7 @@ onUnmounted(() => {
:placeholder="t('dialog.reorganize.episodePartPlaceholder')" :placeholder="t('dialog.reorganize.episodePartPlaceholder')"
:hint="t('dialog.reorganize.episodePartHint')" :hint="t('dialog.reorganize.episodePartHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-file-multiple"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -397,6 +417,7 @@ onUnmounted(() => {
placeholder="0" placeholder="0"
:hint="t('dialog.reorganize.minFileSizeHint')" :hint="t('dialog.reorganize.minFileSizeHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-file-document-outline"
/> />
</VCol> </VCol>
</VRow> </VRow>

View File

@@ -58,23 +58,16 @@ const filteredSites = computed(() => {
<!-- Site Selection Dialog --> <!-- Site Selection Dialog -->
<VDialog max-width="40rem" fullscreen-mobile> <VDialog max-width="40rem" fullscreen-mobile>
<VCard class="site-dialog"> <VCard class="site-dialog">
<VCardTitle class="d-flex align-center pa-4"> <VCardItem>
<span class="text-h6 font-weight-medium">{{ t('dialog.searchSite.selectSites') }}</span> <template #prepend>
<VSpacer /> <VIcon icon="mdi-web-check" />
<VTextField </template>
v-model="siteFilter" <VCardTitle>
:placeholder="t('dialog.searchSite.siteSearch')" {{ t('dialog.searchSite.selectSites') }}
density="compact" </VCardTitle>
variant="outlined" </VCardItem>
hide-details <VDialogCloseBtn @click="emit('close')" />
class="ml-4" <VDivider />
style="max-inline-size: 200px"
prepend-inner-icon="mdi-magnify"
clearable
/>
</VCardTitle>
<VDivider class="search-divider" />
<VCardText style="max-block-size: 420px" class="overflow-y-auto px-4 py-4"> <VCardText style="max-block-size: 420px" class="overflow-y-auto px-4 py-4">
<!-- 站点列表 --> <!-- 站点列表 -->
<div v-if="filteredSites.length > 0"> <div v-if="filteredSites.length > 0">
@@ -164,9 +157,6 @@ const filteredSites = computed(() => {
</VCardText> </VCardText>
<VCardActions class="pt-3"> <VCardActions class="pt-3">
<VBtn color="secondary" @click="emit('close')" class="mr-2 d-flex align-center justify-center">
{{ t('dialog.searchSite.cancel') }}
</VBtn>
<VSpacer /> <VSpacer />
<VBtn <VBtn
color="primary" color="primary"

View File

@@ -148,11 +148,14 @@ onMounted(async () => {
<template> <template>
<VDialog scrollable :close-on-back="false" eager max-width="45rem" :fullscreen="!display.mdAndUp.value"> <VDialog scrollable :close-on-back="false" eager max-width="45rem" :fullscreen="!display.mdAndUp.value">
<VCard <VCard>
:title="`${props.oper === 'add' ? t('site.actions.add') : t('site.actions.edit')}${t('site.title')}${ <VCardItem :class="props.oper === 'add' ? 'py-3' : 'py-2'">
props.oper !== 'add' ? ` - ${siteForm.name}` : '' <template #prepend>
}`" <VIcon :icon="oper == 'add' ? 'mdi-web-plus' : 'mdi-web'" class="me-2" />
> </template>
<VCardTitle>{{ `${props.oper === 'add' ? t('site.actions.add') : t('site.actions.edit')}` }}</VCardTitle>
<VCardSubtitle>{{ siteForm.name }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" /> <VDialogCloseBtn @click="emit('close')" />
<VDivider /> <VDivider />
<VCardText> <VCardText>
@@ -165,6 +168,7 @@ onMounted(async () => {
:rules="[requiredValidator]" :rules="[requiredValidator]"
:hint="t('site.hints.url')" :hint="t('site.hints.url')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-web"
/> />
</VCol> </VCol>
<VCol cols="6" md="3"> <VCol cols="6" md="3">
@@ -175,6 +179,7 @@ onMounted(async () => {
:rules="[requiredValidator]" :rules="[requiredValidator]"
:hint="t('site.hints.priority')" :hint="t('site.hints.priority')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-priority-high"
/> />
</VCol> </VCol>
<VCol cols="6" md="3"> <VCol cols="6" md="3">
@@ -184,6 +189,7 @@ onMounted(async () => {
:label="t('site.fields.status')" :label="t('site.fields.status')"
:hint="t('site.hints.status')" :hint="t('site.hints.status')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-toggle-switch"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -194,6 +200,7 @@ onMounted(async () => {
:label="t('site.fields.rss')" :label="t('site.fields.rss')"
:hint="t('site.hints.rss')" :hint="t('site.hints.rss')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-rss"
/> />
</VCol> </VCol>
<VCol cols="12" md="3"> <VCol cols="12" md="3">
@@ -202,6 +209,7 @@ onMounted(async () => {
:label="t('site.fields.timeout')" :label="t('site.fields.timeout')"
:hint="t('site.hints.timeout')" :hint="t('site.hints.timeout')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-timer"
/> />
</VCol> </VCol>
<VCol cols="6" md="3"> <VCol cols="6" md="3">
@@ -211,6 +219,7 @@ onMounted(async () => {
:items="downloaderOptions" :items="downloaderOptions"
:hint="t('site.hints.downloader')" :hint="t('site.hints.downloader')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-download"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -237,6 +246,7 @@ onMounted(async () => {
:label="t('site.fields.cookie')" :label="t('site.fields.cookie')"
:hint="t('site.hints.cookie')" :hint="t('site.hints.cookie')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-cookie"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
@@ -245,6 +255,7 @@ onMounted(async () => {
:label="t('site.fields.userAgent')" :label="t('site.fields.userAgent')"
:hint="t('site.hints.userAgent')" :hint="t('site.hints.userAgent')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-web-box"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -257,6 +268,7 @@ onMounted(async () => {
:label="t('site.fields.authorization')" :label="t('site.fields.authorization')"
:hint="t('site.hints.authorization')" :hint="t('site.hints.authorization')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-key"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -265,6 +277,7 @@ onMounted(async () => {
:label="t('site.fields.apiKey')" :label="t('site.fields.apiKey')"
:hint="t('site.hints.apiKey')" :hint="t('site.hints.apiKey')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-api"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -283,6 +296,7 @@ onMounted(async () => {
:rules="[numberValidator]" :rules="[numberValidator]"
:hint="t('site.hints.limitInterval')" :hint="t('site.hints.limitInterval')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-clock-outline"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -292,6 +306,7 @@ onMounted(async () => {
:rules="[numberValidator]" :rules="[numberValidator]"
:hint="t('site.hints.limitCount')" :hint="t('site.hints.limitCount')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-counter"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -301,6 +316,7 @@ onMounted(async () => {
:rules="[numberValidator]" :rules="[numberValidator]"
:hint="t('site.hints.limitSeconds')" :hint="t('site.hints.limitSeconds')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-timer-sand"
/> />
</VCol> </VCol>
</VRow> </VRow>

View File

@@ -153,6 +153,7 @@ onMounted(() => {
density="compact" density="compact"
:label="t('dialog.siteResource.searchKeyword')" :label="t('dialog.siteResource.searchKeyword')"
clearable clearable
prepend-inner-icon="mdi-magnify"
/> />
</VCol> </VCol>
<VCol cols="6" md="5"> <VCol cols="6" md="5">
@@ -165,10 +166,13 @@ onMounted(() => {
:label="t('dialog.siteResource.resourceCategory')" :label="t('dialog.siteResource.resourceCategory')"
multiple multiple
clearable clearable
prepend-inner-icon="mdi-folder"
/> />
</VCol> </VCol>
<VCol cols="12" md="2" class="text-center"> <VCol cols="12" md="2" class="text-center">
<VBtn block prepend-icon="mdi-magnify" @click="getResourceList">{{ t('dialog.siteResource.search') }}</VBtn> <VBtn variant="tonal" block prepend-icon="mdi-magnify" @click="getResourceList">
{{ t('dialog.siteResource.search') }}
</VBtn>
</VCol> </VCol>
</VRow> </VRow>
</div> </div>

View File

@@ -281,18 +281,24 @@ onMounted(() => {
<template> <template>
<VDialog scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value"> <VDialog scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value">
<VCard <VCard>
:title=" <VCardItem class="py-2">
props.default <template #prepend>
? t('dialog.subscribeEdit.titleDefault') <VIcon icon="mdi-clipboard-list-outline" class="me-2" />
: t('dialog.subscribeEdit.titleEditFormat', { </template>
name: subscribeForm.name, <VCardTitle>
season: subscribeForm.season {{ props.default ? t('dialog.subscribeEdit.titleDefault') : t('dialog.subscribeEdit.titleEdit') }}
? t('dialog.subscribeEdit.seasonFormat', { number: subscribeForm.season }) </VCardTitle>
: '', <VCardSubtitle v-if="!props.default">
}) {{ subscribeForm.name }}
" <span v-if="subscribeForm.season">
> {{ t('dialog.subscribeEdit.seasonFormat', { number: subscribeForm.season }) }}
</span>
</VCardSubtitle>
<VCardSubtitle v-else>
{{ props.type }}
</VCardSubtitle>
</VCardItem>
<VCardText> <VCardText>
<VDialogCloseBtn @click="emit('close')" /> <VDialogCloseBtn @click="emit('close')" />
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
@@ -314,6 +320,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.searchKeyword')" :label="t('dialog.subscribeEdit.searchKeyword')"
:hint="t('dialog.subscribeEdit.searchKeywordHint')" :hint="t('dialog.subscribeEdit.searchKeywordHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-magnify"
/> />
</VCol> </VCol>
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="4"> <VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="4">
@@ -323,6 +330,7 @@ onMounted(() => {
:rules="[numberValidator]" :rules="[numberValidator]"
:hint="t('dialog.subscribeEdit.totalEpisodeHint')" :hint="t('dialog.subscribeEdit.totalEpisodeHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-playlist-play"
/> />
</VCol> </VCol>
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="4"> <VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="4">
@@ -332,6 +340,7 @@ onMounted(() => {
:rules="[numberValidator]" :rules="[numberValidator]"
:hint="t('dialog.subscribeEdit.startEpisodeHint')" :hint="t('dialog.subscribeEdit.startEpisodeHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-play-circle-outline"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -343,6 +352,7 @@ onMounted(() => {
:items="qualityOptions" :items="qualityOptions"
:hint="t('dialog.subscribeEdit.qualityHint')" :hint="t('dialog.subscribeEdit.qualityHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-quality-high"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -352,6 +362,7 @@ onMounted(() => {
:items="resolutionOptions" :items="resolutionOptions"
:hint="t('dialog.subscribeEdit.resolutionHint')" :hint="t('dialog.subscribeEdit.resolutionHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-monitor"
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -361,6 +372,7 @@ onMounted(() => {
:items="effectOptions" :items="effectOptions"
:hint="t('dialog.subscribeEdit.effectHint')" :hint="t('dialog.subscribeEdit.effectHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-auto-fix"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -375,6 +387,7 @@ onMounted(() => {
clearable clearable
:hint="t('dialog.subscribeEdit.subscribeSitesHint')" :hint="t('dialog.subscribeEdit.subscribeSitesHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-web"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -386,6 +399,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.downloader')" :label="t('dialog.subscribeEdit.downloader')"
:hint="t('dialog.subscribeEdit.downloaderHint')" :hint="t('dialog.subscribeEdit.downloaderHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-download"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -395,6 +409,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.savePath')" :label="t('dialog.subscribeEdit.savePath')"
:hint="t('dialog.subscribeEdit.savePathHint')" :hint="t('dialog.subscribeEdit.savePathHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-folder"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -435,6 +450,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.include')" :label="t('dialog.subscribeEdit.include')"
:hint="t('dialog.subscribeEdit.includeHint')" :hint="t('dialog.subscribeEdit.includeHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-plus-circle-outline"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -443,6 +459,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.exclude')" :label="t('dialog.subscribeEdit.exclude')"
:hint="t('dialog.subscribeEdit.excludeHint')" :hint="t('dialog.subscribeEdit.excludeHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-minus-circle-outline"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -457,6 +474,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.filterGroups')" :label="t('dialog.subscribeEdit.filterGroups')"
:hint="t('dialog.subscribeEdit.filterGroupsHint')" :hint="t('dialog.subscribeEdit.filterGroupsHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-filter"
/> />
</VCol> </VCol>
<VCol v-if="!props.default && subscribeForm.type === '电视剧'" cols="12" md="6"> <VCol v-if="!props.default && subscribeForm.type === '电视剧'" cols="12" md="6">
@@ -467,6 +485,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.episodeGroup')" :label="t('dialog.subscribeEdit.episodeGroup')"
:hint="t('dialog.subscribeEdit.episodeGroupHint')" :hint="t('dialog.subscribeEdit.episodeGroupHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-view-list"
/> />
</VCol> </VCol>
<VCol v-if="!props.default && subscribeForm.type === '电视剧'" cols="12" md="6"> <VCol v-if="!props.default && subscribeForm.type === '电视剧'" cols="12" md="6">
@@ -476,6 +495,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.season')" :label="t('dialog.subscribeEdit.season')"
:hint="t('dialog.subscribeEdit.seasonHint')" :hint="t('dialog.subscribeEdit.seasonHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-calendar"
/> />
</VCol> </VCol>
<VCol cols="12" v-if="!props.default"> <VCol cols="12" v-if="!props.default">
@@ -484,6 +504,7 @@ onMounted(() => {
:label="t('dialog.subscribeEdit.mediaCategory')" :label="t('dialog.subscribeEdit.mediaCategory')"
:hint="t('dialog.subscribeEdit.mediaCategoryHint')" :hint="t('dialog.subscribeEdit.mediaCategoryHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-tag"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -495,6 +516,7 @@ onMounted(() => {
:hint="t('dialog.subscribeEdit.customWordsHint')" :hint="t('dialog.subscribeEdit.customWordsHint')"
persistent-hint persistent-hint
:placeholder="t('dialog.subscribeEdit.customWordsPlaceholder')" :placeholder="t('dialog.subscribeEdit.customWordsPlaceholder')"
prepend-inner-icon="mdi-text"
/> />
</VCol> </VCol>
</VRow> </VRow>

View File

@@ -56,11 +56,18 @@ const $toast = useToast()
<template> <template>
<VDialog scrollable max-width="30rem" :fullscreen="!display.mdAndUp.value"> <VDialog scrollable max-width="30rem" :fullscreen="!display.mdAndUp.value">
<VCard <VCard>
:title="`${t('dialog.subscribeShare.shareSubscription')} - ${props.sub?.name} ${ <VCardItem class="py-2">
props.sub?.season ? t('dialog.subscribeShare.season', { number: props.sub?.season }) : '' <template #prepend>
}`" <VIcon icon="mdi-share-outline" class="me-2" />
> </template>
<VCardTitle>{{ t('dialog.subscribeShare.shareSubscription') }}</VCardTitle>
<VCardSubtitle>
{{ props.sub?.name }}
{{ props.sub?.season ? t('dialog.subscribeShare.season', { number: props.sub?.season }) : '' }}
</VCardSubtitle>
</VCardItem>
<VDivider />
<VCardText> <VCardText>
<VDialogCloseBtn @click="emit('close')" /> <VDialogCloseBtn @click="emit('close')" />
<VForm @submit.prevent="() => {}" class="pt-2"> <VForm @submit.prevent="() => {}" class="pt-2">
@@ -72,6 +79,7 @@ const $toast = useToast()
:label="t('dialog.subscribeShare.title')" :label="t('dialog.subscribeShare.title')"
:rules="[requiredValidator]" :rules="[requiredValidator]"
persistent-hint persistent-hint
prepend-inner-icon="mdi-format-title"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
@@ -81,6 +89,7 @@ const $toast = useToast()
:rules="[requiredValidator]" :rules="[requiredValidator]"
:hint="t('dialog.subscribeShare.descriptionHint')" :hint="t('dialog.subscribeShare.descriptionHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-comment-text-outline"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
@@ -90,6 +99,7 @@ const $toast = useToast()
:rules="[requiredValidator]" :rules="[requiredValidator]"
:hint="t('dialog.subscribeShare.shareUserHint')" :hint="t('dialog.subscribeShare.shareUserHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-account-outline"
/> />
</VCol> </VCol>
</VRow> </VRow>

View File

@@ -119,6 +119,9 @@ onUnmounted(() => {
<VCard> <VCard>
<VDialogCloseBtn @click="emit('close')" /> <VDialogCloseBtn @click="emit('close')" />
<VCardItem> <VCardItem>
<template #prepend>
<VIcon icon="mdi-qrcode" class="me-2" />
</template>
<VCardTitle> <VCardTitle>
{{ t('dialog.u115Auth.loginTitle') }} {{ t('dialog.u115Auth.loginTitle') }}
</VCardTitle> </VCardTitle>

View File

@@ -291,11 +291,14 @@ onMounted(() => {
<template> <template>
<VDialog scrollable max-width="40rem" :fullscreen="!display.mdAndUp.value"> <VDialog scrollable max-width="40rem" :fullscreen="!display.mdAndUp.value">
<VCard <VCard>
:title="`${props.oper === 'add' ? t('dialog.userAddEdit.add') : t('dialog.userAddEdit.edit')}${ <VCardItem :class="props.oper === 'add' ? 'py-3' : 'py-2'">
props.oper !== 'add' ? ` - ${userName}` : '' <template #prepend>
}`" <VIcon icon="mdi-account" class="me-2" />
> </template>
<VCardTitle>{{ props.oper === 'add' ? t('dialog.userAddEdit.add') : t('dialog.userAddEdit.edit') }}</VCardTitle>
<VCardSubtitle>{{ userName }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" /> <VDialogCloseBtn @click="emit('close')" />
<VDivider /> <VDivider />
<VCardItem> <VCardItem>
@@ -350,6 +353,7 @@ onMounted(() => {
density="comfortable" density="comfortable"
:readonly="props.oper !== 'add'" :readonly="props.oper !== 'add'"
:label="t('dialog.userAddEdit.username')" :label="t('dialog.userAddEdit.username')"
prepend-inner-icon="mdi-account"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -359,6 +363,7 @@ onMounted(() => {
clearable clearable
:label="t('dialog.userAddEdit.email')" :label="t('dialog.userAddEdit.email')"
type="email" type="email"
prepend-inner-icon="mdi-email"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -370,6 +375,7 @@ onMounted(() => {
clearable clearable
:label="t('dialog.userAddEdit.password')" :label="t('dialog.userAddEdit.password')"
autocomplete="" autocomplete=""
prepend-inner-icon="mdi-lock"
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible" @click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
/> />
</VCol> </VCol>
@@ -382,6 +388,7 @@ onMounted(() => {
:append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'" :append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
clearable clearable
:label="t('dialog.userAddEdit.confirmPassword')" :label="t('dialog.userAddEdit.confirmPassword')"
prepend-inner-icon="mdi-lock-check"
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible" @click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
/> />
</VCol> </VCol>
@@ -392,6 +399,7 @@ onMounted(() => {
clearable clearable
:label="t('dialog.userAddEdit.nickname')" :label="t('dialog.userAddEdit.nickname')"
placeholder="显示昵称,优先于用户名显示" placeholder="显示昵称,优先于用户名显示"
prepend-inner-icon="mdi-card-account-details"
/> />
</VCol> </VCol>
<VCol cols="12" md="6" v-if="canControl"> <VCol cols="12" md="6" v-if="canControl">
@@ -402,6 +410,7 @@ onMounted(() => {
item-value="value" item-value="value"
:label="t('dialog.userAddEdit.status')" :label="t('dialog.userAddEdit.status')"
dense dense
prepend-inner-icon="mdi-toggle-switch"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -415,6 +424,7 @@ onMounted(() => {
density="comfortable" density="comfortable"
clearable clearable
:label="t('dialog.userAddEdit.wechat')" :label="t('dialog.userAddEdit.wechat')"
prepend-inner-icon="mdi-wechat"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -423,6 +433,7 @@ onMounted(() => {
density="comfortable" density="comfortable"
clearable clearable
:label="t('dialog.userAddEdit.telegram')" :label="t('dialog.userAddEdit.telegram')"
prepend-inner-icon="mdi-send"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -431,6 +442,7 @@ onMounted(() => {
density="comfortable" density="comfortable"
clearable clearable
:label="t('dialog.userAddEdit.slack')" :label="t('dialog.userAddEdit.slack')"
prepend-inner-icon="mdi-slack"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -439,6 +451,7 @@ onMounted(() => {
density="comfortable" density="comfortable"
clearable clearable
:label="t('dialog.userAddEdit.vocechat')" :label="t('dialog.userAddEdit.vocechat')"
prepend-inner-icon="mdi-chat"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -447,10 +460,17 @@ onMounted(() => {
density="comfortable" density="comfortable"
clearable clearable
:label="t('dialog.userAddEdit.synologyChat')" :label="t('dialog.userAddEdit.synologyChat')"
prepend-inner-icon="mdi-message"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField v-model="userForm.settings.douban_userid" density="comfortable" clearable label="豆瓣用户" /> <VTextField
v-model="userForm.settings.douban_userid"
density="comfortable"
clearable
label="豆瓣用户"
prepend-inner-icon="mdi-movie"
/>
</VCol> </VCol>
</VRow> </VRow>
</VForm> </VForm>

View File

@@ -138,8 +138,15 @@ onMounted(async () => {
<template> <template>
<VDialog width="40rem" scrollable :fullscreen="!display.mdAndUp.value"> <VDialog width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard :title="t('dialog.userAuth.title')"> <VCard>
<VDialogCloseBtn @click="emit('close')" /> <VCardItem>
<VCardTitle>
<VIcon icon="mdi-user-check" class="me-2" />
{{ t('dialog.userAuth.title') }}
</VCardTitle>
<VDialogCloseBtn @click="emit('close')" />
</VCardItem>
<VDivider />
<VCardText> <VCardText>
<VRow> <VRow>
<VCol cols="12"> <VCol cols="12">
@@ -150,6 +157,7 @@ onMounted(async () => {
item-title="name" item-title="name"
:label="t('dialog.userAuth.selectSite')" :label="t('dialog.userAuth.selectSite')"
item-props item-props
prepend-inner-icon="mdi-web"
> >
</VSelect> </VSelect>
</VCol> </VCol>

View File

@@ -86,7 +86,13 @@ async function editWorkflow() {
<template> <template>
<VDialog scrollable :close-on-back="false" eager max-width="30rem" :fullscreen="!display.mdAndUp.value"> <VDialog scrollable :close-on-back="false" eager max-width="30rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="title"> <VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-clock-outline" class="me-2" />
</template>
<VCardTitle>{{ title }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" /> <VDialogCloseBtn @click="emit('close')" />
<VDivider /> <VDivider />
<VCardText> <VCardText>
@@ -99,6 +105,7 @@ async function editWorkflow() {
:rules="[requiredValidator]" :rules="[requiredValidator]"
persistent-hint persistent-hint
:hint="t('dialog.workflowAddEdit.namePlaceholder')" :hint="t('dialog.workflowAddEdit.namePlaceholder')"
prepend-inner-icon="mdi-workflow"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
@@ -109,6 +116,7 @@ async function editWorkflow() {
placeholder="5位cron表达式" placeholder="5位cron表达式"
persistent-hint persistent-hint
:hint="t('dialog.workflowAddEdit.cronExprDesc')" :hint="t('dialog.workflowAddEdit.cronExprDesc')"
prepend-inner-icon="mdi-clock-outline"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
@@ -116,6 +124,7 @@ async function editWorkflow() {
v-model="workflowForm.description" v-model="workflowForm.description"
:label="t('dialog.workflowAddEdit.desc')" :label="t('dialog.workflowAddEdit.desc')"
:placeholder="t('dialog.workflowAddEdit.descPlaceholder')" :placeholder="t('dialog.workflowAddEdit.descPlaceholder')"
prepend-inner-icon="mdi-text-box-outline"
/> />
</VCol> </VCol>
</VRow> </VRow>

View File

@@ -696,13 +696,24 @@ onMounted(() => {
</VCard> </VCard>
<!-- 重命名弹窗 --> <!-- 重命名弹窗 -->
<VDialog v-if="renamePopper" v-model="renamePopper" max-width="35rem"> <VDialog v-if="renamePopper" v-model="renamePopper" max-width="35rem">
<VCard :title="t('file.rename')"> <VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-pencil" class="me-2" />
</template>
<VCardTitle>{{ t('file.rename') }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="renamePopper = false" /> <VDialogCloseBtn @click="renamePopper = false" />
<VDivider /> <VDivider />
<VCardText> <VCardText>
<VRow> <VRow>
<VCol cols="12"> <VCol cols="12">
<VTextField v-model="newName" :label="t('file.newName')" :loading="renameLoading" /> <VTextField
v-model="newName"
:label="t('file.newName')"
:loading="renameLoading"
prepend-inner-icon="mdi-format-text"
/>
</VCol> </VCol>
<VCol cols="12" v-if="currentItem && currentItem.type == 'dir'"> <VCol cols="12" v-if="currentItem && currentItem.type == 'dir'">
<VSwitch v-model="renameAll" :label="t('file.includeSubfolders')" /> <VSwitch v-model="renameAll" :label="t('file.includeSubfolders')" />

View File

@@ -165,17 +165,24 @@ const sortIcon = computed(() => {
<IconBtn v-if="pathSegments.length > 0" @click="goUp"> <IconBtn v-if="pathSegments.length > 0" @click="goUp">
<VIcon icon="mdi-arrow-up-bold-outline" /> <VIcon icon="mdi-arrow-up-bold-outline" />
</IconBtn> </IconBtn>
<!-- 新建文件夹 -->
<VDialog v-model="newFolderPopper" max-width="35rem"> <VDialog v-model="newFolderPopper" max-width="35rem">
<template #activator="{ props }"> <template #activator="{ props }">
<IconBtn> <IconBtn>
<VIcon v-bind="props" icon="mdi-folder-plus-outline" /> <VIcon v-bind="props" icon="mdi-folder-plus-outline" />
</IconBtn> </IconBtn>
</template> </template>
<VCard :title="t('file.newFolder')"> <VCard>
<VCardItem>
<template #prepend>
<VIcon icon="mdi-folder-plus-outline" class="me-2" />
</template>
<VCardTitle>{{ t('file.newFolder') }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="newFolderPopper = false" /> <VDialogCloseBtn @click="newFolderPopper = false" />
<VDivider /> <VDivider />
<VCardText> <VCardText>
<VTextField v-model="newFolderName" :label="t('common.name')" /> <VTextField v-model="newFolderName" :label="t('common.name')" prepend-inner-icon="mdi-format-text" />
</VCardText> </VCardText>
<VCardActions> <VCardActions>
<div class="flex-grow-1" /> <div class="flex-grow-1" />

View File

@@ -270,7 +270,7 @@ onMounted(() => {
</VCardItem> </VCardItem>
<VDivider /> <VDivider />
<VCardText> <VCardText>
<LoggingView /> <LoggingView logfile="moviepilot.log" />
</VCardText> </VCardText>
</VCard> </VCard>
</VDialog> </VDialog>

View File

@@ -39,6 +39,11 @@ export default {
unsubscribe: 'Unsubscribe', unsubscribe: 'Unsubscribe',
media: 'Media', media: 'Media',
unknown: 'Unknown', unknown: 'Unknown',
notice: 'Notice',
itemsPerPage: 'Items per page',
pageText: '{0}-{1} of {2}',
noDataText: 'No data',
loadingText: 'Loading...',
}, },
mediaType: { mediaType: {
movie: 'Movie', movie: 'Movie',
@@ -201,6 +206,10 @@ export default {
title: 'Services', title: 'Services',
description: 'Scheduled jobs', description: 'Scheduled jobs',
}, },
cache: {
title: 'Cache',
description: 'Torrent cache, media recognition data cache, image file cache management',
},
notification: { notification: {
title: 'Notifications', title: 'Notifications',
description: 'Notification channels (WeChat, Telegram, Slack, SynologyChat, VoceChat, WebPush), message scope', description: 'Notification channels (WeChat, Telegram, Slack, SynologyChat, VoceChat, WebPush), message scope',
@@ -852,8 +861,8 @@ export default {
browserSimulation: 'Use browser simulation for authentic site access', browserSimulation: 'Use browser simulation for authentic site access',
}, },
actions: { actions: {
add: 'Add', add: 'Add Site',
edit: 'Edit', edit: 'Edit Site',
}, },
messages: { messages: {
addSuccess: 'Site added successfully', addSuccess: 'Site added successfully',
@@ -1095,10 +1104,16 @@ export default {
securityImageDomainsHint: 'Allowed image domains whitelist for caching, used to control trusted image sources', securityImageDomainsHint: 'Allowed image domains whitelist for caching, used to control trusted image sources',
noSecurityImageDomains: 'No security domains', noSecurityImageDomains: 'No security domains',
securityImageDomainAdd: 'Add domain, e.g.: image.tmdb.org', securityImageDomainAdd: 'Add domain, e.g.: image.tmdb.org',
proxyHost: 'Proxy Server',
proxyHostHint: 'Set proxy server address, support: http(s), socks5, socks5h, etc.',
moviePilotAutoUpdate: 'Auto Update MoviePilot',
moviePilotAutoUpdateHint: 'Automatically update MoviePilot to the latest release version when restarting',
autoUpdateResource: 'Auto Update Resource',
autoUpdateResourceHint: 'Automatically detect and update site resource package when restarting',
}, },
site: { site: {
siteSync: 'Site Synchronization', siteSync: 'Site Synchronization',
siteSyncDesc: 'Quickly sync site data from CookieCloud.', siteSyncDesc: 'Quickly sync site data from CookieCloud',
enableLocalCookieCloud: 'Enable Local CookieCloud Server', enableLocalCookieCloud: 'Enable Local CookieCloud Server',
enableLocalCookieCloudHint: enableLocalCookieCloudHint:
'Use built-in CookieCloud service to sync site data, service address: http://localhost:3000/cookiecloud', 'Use built-in CookieCloud service to sync site data, service address: http://localhost:3000/cookiecloud',
@@ -1144,7 +1159,7 @@ export default {
}, },
notification: { notification: {
channels: 'Notification Channels', channels: 'Notification Channels',
channelsDesc: 'Set message sending channel parameters.', channelsDesc: 'Set message sending channel parameters',
organizeSuccess: 'Media Import', organizeSuccess: 'Media Import',
downloadAdded: 'Download Added', downloadAdded: 'Download Added',
subscribeAdded: 'Subscribe Added', subscribeAdded: 'Subscribe Added',
@@ -1191,7 +1206,7 @@ export default {
}, },
words: { words: {
customIdentifiers: 'Custom Identifiers', customIdentifiers: 'Custom Identifiers',
identifiersDesc: 'Add rules to preprocess torrent names or file names to correct identification.', identifiersDesc: 'Add rules to preprocess torrent names or file names to correct identification',
identifiersPlaceholder: 'Support regular expressions, special characters need \\ escape, one line for each rule', identifiersPlaceholder: 'Support regular expressions, special characters need \\ escape, one line for each rule',
identifiersHint: 'Support regular expressions, special characters need \\ escape, one line for each rule', identifiersHint: 'Support regular expressions, special characters need \\ escape, one line for each rule',
formatTitle: 'Supported configuration formats (mind the spaces):', formatTitle: 'Supported configuration formats (mind the spaces):',
@@ -1231,7 +1246,7 @@ export default {
}, },
search: { search: {
basicSettings: 'Basic Settings', basicSettings: 'Basic Settings',
basicSettingsDesc: 'Set data sources, rule groups and other basic information.', basicSettingsDesc: 'Set data sources, rule groups and other basic information',
recognizeSource: 'Recognition Data Source', recognizeSource: 'Recognition Data Source',
recognizeSourceDesc: recognizeSourceDesc:
'Default is TMDB. Douban is usually more friendly for Chinese works, but some foreign works have incomplete information.', 'Default is TMDB. Douban is usually more friendly for Chinese works, but some foreign works have incomplete information.',
@@ -1332,8 +1347,7 @@ export default {
}, },
scheduler: { scheduler: {
title: 'Scheduled Jobs', title: 'Scheduled Jobs',
subtitle: subtitle: 'Includes built-in system services and plugin services',
"Includes built-in system services and plugin services. Manual execution will not affect the job's normal schedule.",
provider: 'Provider', provider: 'Provider',
taskName: 'Task Name', taskName: 'Task Name',
taskStatus: 'Task Status', taskStatus: 'Task Status',
@@ -1380,6 +1394,55 @@ export default {
settingsSaveSuccess: 'Subscription basic settings saved successfully', settingsSaveSuccess: 'Subscription basic settings saved successfully',
settingsSaveFailed: 'Failed to save subscription basic settings!', settingsSaveFailed: 'Failed to save subscription basic settings!',
}, },
cache: {
title: 'Cache Management',
subtitle: 'Manage torrent cache data',
filterByTitle: 'Filter by Title',
filterBySite: 'Filter by Site',
selectSite: 'Select Site',
refresh: 'Refresh Cache',
deleteSelected: 'Delete Selected',
clearAll: 'Clear All Cache',
refreshSuccess: 'Cache refresh completed',
refreshFailed: 'Failed to refresh cache',
clearSuccess: 'Cache clear completed',
clearFailed: 'Failed to clear cache',
deleteSuccess: 'Cache item deleted successfully',
deleteFailed: 'Failed to delete cache item',
deleteSelectedSuccess: 'Successfully deleted {count} cache items',
deleteSelectedFailed: 'Failed to delete cache items',
loadFailed: 'Failed to load cache data',
selectDeleteWarning: 'Please select cache items to delete',
reidentify: 'Re-identify',
reidentifySuccess: 'Re-identification completed',
reidentifyFailed: 'Re-identification failed',
poster: 'Poster',
torrentTitle: 'Title',
site: 'Site',
size: 'Size',
publishTime: 'Publish Time',
recognitionResult: 'Recognition Result',
actions: 'Actions',
unrecognized: 'Unrecognized',
noData: 'No cache data',
noDataHint: 'Click "Refresh Cache" button to get the latest torrent cache',
reidentifyDialog: {
title: 'Re-identify',
torrentInfo: 'Torrent Info',
tmdbId: 'TMDB ID',
tmdbIdHint: 'Optional, manually specify TMDB ID for recognition',
doubanId: 'Douban ID',
doubanIdHint: 'Optional, manually specify Douban ID for recognition',
autoHint: 'If no ID is specified, the torrent will be automatically re-identified',
cancel: 'Cancel',
confirm: 'Re-identify',
},
mediaType: {
movie: 'Movie',
tv: 'TV Show',
},
clearConfirm: 'Are you sure you want to clear all cache?',
},
}, },
dialog: { dialog: {
progress: { progress: {
@@ -1703,8 +1766,8 @@ export default {
previous: 'Previous', previous: 'Previous',
confirm: 'Confirm', confirm: 'Confirm',
manualTitle: 'Manual Organization', manualTitle: 'Manual Organization',
multipleItemsTitle: 'Organize - {count} Items', multipleItemsTitle: '{count} Items',
singleItemTitle: 'Organize - {path}', singleItemTitle: '{path}',
targetStorage: 'Target Storage', targetStorage: 'Target Storage',
targetStorageHint: 'Organization target storage', targetStorageHint: 'Organization target storage',
transferType: 'Organization Method', transferType: 'Organization Method',
@@ -1753,7 +1816,7 @@ export default {
}, },
subscribeEdit: { subscribeEdit: {
titleDefault: 'Default Subscription Rules', titleDefault: 'Default Subscription Rules',
titleEditFormat: 'Edit Subscription - {name} {season}', titleEdit: 'Edit Subscription',
seasonFormat: 'Season {number}', seasonFormat: 'Season {number}',
tabs: { tabs: {
basic: 'Basic', basic: 'Basic',
@@ -2013,6 +2076,40 @@ export default {
folderName: 'Folder Name', folderName: 'Folder Name',
cancel: 'Cancel', cancel: 'Cancel',
create: 'Create', create: 'Create',
clone: 'Clone',
cloneTitle: 'Create Plugin Clone',
cloneSubtitle: 'Create an independent clone instance for {name}',
cloneFeature: 'Plugin Clone Feature',
cloneDescription:
'Create an independent copy of the plugin with separate configuration and data, suitable for multi-account, testing environments, etc.',
suffix: 'Clone Suffix',
suffixPlaceholder: 'e.g.: Test, Backup, Site1',
suffixHint: 'Unique identifier to distinguish clones, only letters and numbers allowed',
suffixRequired: 'Clone suffix cannot be empty',
suffixFormatError: 'Only letters and numbers allowed',
suffixLengthError: 'Length cannot exceed 20 characters',
cloneName: 'Clone Name',
cloneNamePlaceholder: 'e.g.: Auto Backup Test Version',
cloneNameHint: 'Display name for the clone plugin (optional)',
cloneDefaultName: '{name} Clone',
cloneDescriptionLabel: 'Clone Description',
cloneDescriptionPlaceholder: 'Describe the purpose and features of this clone...',
cloneDescriptionHint: 'Detailed description of the clone plugin purpose (optional)',
cloneDefaultDescription: '{description} (Clone Version)',
cloneVersion: 'Version',
cloneVersionPlaceholder: 'e.g.: 1.0, 2.1.0',
cloneVersionHint: 'Custom version number for the clone plugin (optional)',
cloneIcon: 'Icon URL',
cloneIconPlaceholder: 'https://example.com/icon.png',
cloneIconHint: 'Custom icon for the clone plugin (optional)',
cloneNotice:
'Clone plugins are disabled by default after creation and need to be manually configured and enabled. The clone suffix cannot be modified once set.',
createClone: 'Create Clone',
cloning: 'Creating clone for {name}...',
cloneSuccess: 'Plugin clone {name} created successfully!',
cloneFailed: 'Plugin clone creation failed: {message}',
cloneFailedGeneral: 'Plugin clone creation failed',
logTitle: 'Plugin Logging',
}, },
profile: { profile: {
personalInfo: 'Personal Information', personalInfo: 'Personal Information',

View File

@@ -39,6 +39,11 @@ export default {
unsubscribe: '取消订阅', unsubscribe: '取消订阅',
media: '媒体', media: '媒体',
unknown: '未知', unknown: '未知',
notice: '注意',
itemsPerPage: '每页条数',
pageText: '{0}-{1} 共 {2} 条',
noDataText: '没有数据',
loadingText: '加载中...',
}, },
mediaType: { mediaType: {
movie: '电影', movie: '电影',
@@ -201,6 +206,10 @@ export default {
title: '服务', title: '服务',
description: '定时作业', description: '定时作业',
}, },
cache: {
title: '缓存',
description: '种子缓存、图片文件缓存管理',
},
notification: { notification: {
title: '通知', title: '通知',
description: '通知渠道微信、Telegram、Slack、SynologyChat、VoceChat、WebPush、消息发送范围', description: '通知渠道微信、Telegram、Slack、SynologyChat、VoceChat、WebPush、消息发送范围',
@@ -849,8 +858,8 @@ export default {
browserSimulation: '使用浏览器模拟真实访问该站点', browserSimulation: '使用浏览器模拟真实访问该站点',
}, },
actions: { actions: {
add: '新增', add: '新增站点',
edit: '编辑', edit: '编辑站点',
}, },
messages: { messages: {
addSuccess: '新增站点成功', addSuccess: '新增站点成功',
@@ -936,7 +945,7 @@ export default {
system: { system: {
custom: '自定义', custom: '自定义',
basicSettings: '基础设置', basicSettings: '基础设置',
basicSettingsDesc: '设置服务器的全局功能', basicSettingsDesc: '设置服务器的全局功能',
appDomain: '访问域名', appDomain: '访问域名',
appDomainHint: '用于发送通知时,添加快捷跳转地址', appDomainHint: '用于发送通知时,添加快捷跳转地址',
wallpaper: '背景壁纸', wallpaper: '背景壁纸',
@@ -1085,10 +1094,16 @@ export default {
securityImageDomainsHint: '允许缓存的图片域名白名单,用于控制可信任的图片来源', securityImageDomainsHint: '允许缓存的图片域名白名单,用于控制可信任的图片来源',
noSecurityImageDomains: '暂无安全域名', noSecurityImageDomains: '暂无安全域名',
securityImageDomainAdd: '添加域名image.tmdb.org', securityImageDomainAdd: '添加域名image.tmdb.org',
proxyHost: '代理服务器',
proxyHostHint: '设置代理服务器地址支持http(s)、socks5、socks5h 等协议',
moviePilotAutoUpdate: '自动更新MoviePilot',
moviePilotAutoUpdateHint: '重启时自动更新MoviePilot到最新发行版本',
autoUpdateResource: '自动更新站点资源',
autoUpdateResourceHint: '重启时自动检测和更新站点资源包',
}, },
site: { site: {
siteSync: '站点同步', siteSync: '站点同步',
siteSyncDesc: '从CookieCloud快速同步站点数据', siteSyncDesc: '从CookieCloud快速同步站点数据',
enableLocalCookieCloud: '启用本地CookieCloud服务器', enableLocalCookieCloud: '启用本地CookieCloud服务器',
enableLocalCookieCloudHint: '使用内建CookieCloud服务同步站点数据服务地址为http://localhost:3000/cookiecloud', enableLocalCookieCloudHint: '使用内建CookieCloud服务同步站点数据服务地址为http://localhost:3000/cookiecloud',
serviceAddress: '服务地址', serviceAddress: '服务地址',
@@ -1131,7 +1146,7 @@ export default {
}, },
notification: { notification: {
channels: '通知渠道', channels: '通知渠道',
channelsDesc: '设置消息发送渠道参数', channelsDesc: '设置消息发送渠道参数',
organizeSuccess: '资源入库', organizeSuccess: '资源入库',
downloadAdded: '资源下载', downloadAdded: '资源下载',
subscribeAdded: '添加订阅', subscribeAdded: '添加订阅',
@@ -1178,7 +1193,7 @@ export default {
}, },
words: { words: {
customIdentifiers: '自定义识别词', customIdentifiers: '自定义识别词',
identifiersDesc: '添加规则对种子名或者文件名进行预处理以校正识别', identifiersDesc: '添加规则对种子名或者文件名进行预处理以校正识别',
identifiersPlaceholder: '支持正则表达式,特殊字符需要\\转义,一行为一组', identifiersPlaceholder: '支持正则表达式,特殊字符需要\\转义,一行为一组',
identifiersHint: '支持正则表达式,特殊字符需要\\转义,一行为一组', identifiersHint: '支持正则表达式,特殊字符需要\\转义,一行为一组',
formatTitle: '支持的配置格式(注意空格):', formatTitle: '支持的配置格式(注意空格):',
@@ -1214,7 +1229,7 @@ export default {
}, },
search: { search: {
basicSettings: '基础设置', basicSettings: '基础设置',
basicSettingsDesc: '设定数据源、规则组等基础信息', basicSettingsDesc: '设定数据源、规则组等基础信息',
recognizeSource: '识别数据源', recognizeSource: '识别数据源',
recognizeSourceDesc: '默认使用TMDB。豆瓣识别中文作品通常更友好但有些国外作品信息不完整。', recognizeSourceDesc: '默认使用TMDB。豆瓣识别中文作品通常更友好但有些国外作品信息不完整。',
themoviedb: 'TheMovieDb', themoviedb: 'TheMovieDb',
@@ -1250,7 +1265,7 @@ export default {
}, },
directory: { directory: {
storage: '存储', storage: '存储',
storageDesc: '设置本地或网盘存储', storageDesc: '设置本地或网盘存储',
directory: '目录', directory: '目录',
mediaType: '媒体类型', mediaType: '媒体类型',
directoryDesc: '设置媒体文件整理目录结构,按先后顺序依次匹配。', directoryDesc: '设置媒体文件整理目录结构,按先后顺序依次匹配。',
@@ -1312,7 +1327,7 @@ export default {
}, },
scheduler: { scheduler: {
title: '定时作业', title: '定时作业',
subtitle: '包含系统内置服务以及插件提供的服务,手动执行不会影响作业正常的时间表。', subtitle: '包含系统内置服务以及插件提供的服务',
provider: '提供者', provider: '提供者',
taskName: '任务名称', taskName: '任务名称',
taskStatus: '任务状态', taskStatus: '任务状态',
@@ -1359,6 +1374,55 @@ export default {
settingsSaveSuccess: '订阅基础设置保存成功', settingsSaveSuccess: '订阅基础设置保存成功',
settingsSaveFailed: '订阅基础设置保存失败!', settingsSaveFailed: '订阅基础设置保存失败!',
}, },
cache: {
title: '缓存管理',
subtitle: '管理缓存的站点资源',
filterByTitle: '按标题筛选',
filterBySite: '按站点筛选',
selectSite: '选择站点',
refresh: '刷新缓存',
deleteSelected: '删除选中',
clearAll: '清空缓存',
refreshSuccess: '缓存刷新完成',
refreshFailed: '刷新缓存失败',
clearSuccess: '缓存清理完成',
clearFailed: '清理缓存失败',
deleteSuccess: '缓存项删除成功',
deleteFailed: '删除缓存项失败',
deleteSelectedSuccess: '成功删除 {count} 个缓存项',
deleteSelectedFailed: '删除缓存项失败',
loadFailed: '加载缓存数据失败',
selectDeleteWarning: '请选择要删除的缓存项',
reidentify: '重新识别',
reidentifySuccess: '重新识别完成',
reidentifyFailed: '重新识别失败',
poster: '海报',
torrentTitle: '标题',
site: '站点',
size: '大小',
publishTime: '发布时间',
recognitionResult: '识别结果',
actions: '操作',
unrecognized: '未识别',
noData: '暂无缓存数据',
noDataHint: '点击"刷新缓存"按钮获取最新的种子缓存',
reidentifyDialog: {
title: '重新识别',
torrentInfo: '种子信息',
tmdbId: 'TMDB ID',
tmdbIdHint: '可选手动指定TMDB ID进行识别',
doubanId: '豆瓣 ID',
doubanIdHint: '可选手动指定豆瓣ID进行识别',
autoHint: '如果不指定ID将自动重新识别该种子',
cancel: '取消',
confirm: '重新识别',
},
mediaType: {
movie: '电影',
tv: '电视剧',
},
clearConfirm: '确认清空所有缓存吗?',
},
}, },
dialog: { dialog: {
progress: { progress: {
@@ -1680,8 +1744,8 @@ export default {
previous: '上一步', previous: '上一步',
confirm: '确认', confirm: '确认',
manualTitle: '手动整理', manualTitle: '手动整理',
multipleItemsTitle: '整理 - 共 {count} 项', multipleItemsTitle: '共 {count} 项',
singleItemTitle: '整理 - {path}', singleItemTitle: '{path}',
targetStorage: '目的存储', targetStorage: '目的存储',
targetStorageHint: '整理目的存储', targetStorageHint: '整理目的存储',
transferType: '整理方式', transferType: '整理方式',
@@ -1730,7 +1794,7 @@ export default {
}, },
subscribeEdit: { subscribeEdit: {
titleDefault: '默认订阅规则', titleDefault: '默认订阅规则',
titleEditFormat: '编辑订阅 - {name} {season}', titleEdit: '编辑订阅',
seasonFormat: '第 {number} 季', seasonFormat: '第 {number} 季',
tabs: { tabs: {
basic: '基础', basic: '基础',
@@ -1989,6 +2053,38 @@ export default {
folderName: '文件夹名称', folderName: '文件夹名称',
cancel: '取消', cancel: '取消',
create: '创建', create: '创建',
clone: '分身',
cloneTitle: '创建插件分身',
cloneSubtitle: '为 {name} 创建独立的分身实例',
cloneFeature: '插件分身功能',
cloneDescription: '创建插件的独立副本,拥有独立的配置和数据,适用于多账号、测试环境等场景',
suffix: '分身后缀',
suffixPlaceholder: '例如Test、Backup、Site1',
suffixHint: '用于区分分身的唯一标识,只能包含英文字母和数字',
suffixRequired: '分身后缀不能为空',
suffixFormatError: '只能包含英文字母和数字',
suffixLengthError: '长度不能超过20个字符',
cloneName: '分身名称',
cloneNamePlaceholder: '例如:自动备份 测试版',
cloneNameHint: '分身插件的显示名称(可选)',
cloneDefaultName: '{name} 分身',
cloneDescriptionLabel: '分身描述',
cloneDescriptionPlaceholder: '描述这个分身的用途和特点...',
cloneDescriptionHint: '详细描述分身插件的用途(可选)',
cloneDefaultDescription: '{description} (分身版本)',
cloneVersion: '版本号',
cloneVersionPlaceholder: '例如1.0、2.1.0',
cloneVersionHint: '自定义分身插件的版本号(可选)',
cloneIcon: '图标URL',
cloneIconPlaceholder: 'https://example.com/icon.png',
cloneIconHint: '自定义分身插件的图标(可选)',
cloneNotice: '分身插件创建后默认为禁用状态,需要手动配置启用。分身后缀一旦确定无法修改。',
createClone: '创建分身',
cloning: '正在创建 {name} 的分身...',
cloneSuccess: '插件分身 {name} 创建成功!',
cloneFailed: '插件分身创建失败:{message}',
cloneFailedGeneral: '插件分身创建失败',
logTitle: '插件日志',
}, },
profile: { profile: {
personalInfo: '个人信息', personalInfo: '个人信息',

View File

@@ -39,6 +39,11 @@ export default {
unsubscribe: '取消訂閱', unsubscribe: '取消訂閱',
media: '媒體', media: '媒體',
unknown: '未知', unknown: '未知',
notice: '注意',
itemsPerPage: '每頁條數',
pageText: '{0}-{1} 共 {2} 條',
noDataText: '沒有數據',
loadingText: '加載中...',
}, },
mediaType: { mediaType: {
movie: '電影', movie: '電影',
@@ -202,6 +207,10 @@ export default {
title: '服務', title: '服務',
description: '定時作業', description: '定時作業',
}, },
cache: {
title: '緩存',
description: '種子緩存、識別媒體數據緩存、圖片文件緩存管理',
},
notification: { notification: {
title: '通知', title: '通知',
description: '通知渠道微信、Telegram、Slack、SynologyChat、VoceChat、WebPush、消息發送範圍', description: '通知渠道微信、Telegram、Slack、SynologyChat、VoceChat、WebPush、消息發送範圍',
@@ -851,8 +860,8 @@ export default {
browserSimulation: '使用瀏覽器模擬真實訪問該站點', browserSimulation: '使用瀏覽器模擬真實訪問該站點',
}, },
actions: { actions: {
add: '新增', add: '新增站點',
edit: '編輯', edit: '編輯站點',
}, },
messages: { messages: {
addSuccess: '新增站點成功', addSuccess: '新增站點成功',
@@ -938,7 +947,7 @@ export default {
system: { system: {
custom: '自定義', custom: '自定義',
basicSettings: '基礎設置', basicSettings: '基礎設置',
basicSettingsDesc: '設置服務器的全局功能', basicSettingsDesc: '設置服務器的全局功能',
appDomain: '訪問域名', appDomain: '訪問域名',
appDomainHint: '用於發送通知時,添加快捷跳轉地址', appDomainHint: '用於發送通知時,添加快捷跳轉地址',
wallpaper: '背景壁紙', wallpaper: '背景壁紙',
@@ -1087,10 +1096,16 @@ export default {
securityImageDomainsHint: '允許緩存的圖片域名白名單,用於控制可信任的圖片來源', securityImageDomainsHint: '允許緩存的圖片域名白名單,用於控制可信任的圖片來源',
noSecurityImageDomains: '暫無安全域名', noSecurityImageDomains: '暫無安全域名',
securityImageDomainAdd: '添加域名image.tmdb.org', securityImageDomainAdd: '添加域名image.tmdb.org',
proxyHost: '代理服務器',
proxyHostHint: '設置代理服務器地址支持http(s)、socks5、socks5h 等協議',
moviePilotAutoUpdate: '自動更新MoviePilot',
moviePilotAutoUpdateHint: '重啟時自動更新MoviePilot到最新發行版本',
autoUpdateResource: '自動更新站點資源',
autoUpdateResourceHint: '重啟時自動檢測和更新站點資源包',
}, },
site: { site: {
siteSync: '站點同步', siteSync: '站點同步',
siteSyncDesc: '從CookieCloud快速同步站點數據', siteSyncDesc: '從CookieCloud快速同步站點數據',
enableLocalCookieCloud: '啟用本地CookieCloud服務器', enableLocalCookieCloud: '啟用本地CookieCloud服務器',
enableLocalCookieCloudHint: '使用內建CookieCloud服務同步站點數據服務地址為http://localhost:3000/cookiecloud', enableLocalCookieCloudHint: '使用內建CookieCloud服務同步站點數據服務地址為http://localhost:3000/cookiecloud',
serviceAddress: '服務地址', serviceAddress: '服務地址',
@@ -1133,7 +1148,7 @@ export default {
}, },
notification: { notification: {
channels: '通知渠道', channels: '通知渠道',
channelsDesc: '設置消息發送渠道參數', channelsDesc: '設置消息發送渠道參數',
organizeSuccess: '資源入庫', organizeSuccess: '資源入庫',
downloadAdded: '資源下載', downloadAdded: '資源下載',
subscribeAdded: '添加訂閱', subscribeAdded: '添加訂閱',
@@ -1180,7 +1195,7 @@ export default {
}, },
words: { words: {
customIdentifiers: '自定義識別詞', customIdentifiers: '自定義識別詞',
identifiersDesc: '添加規則對種子名或者文件名進行預處理以校正識別', identifiersDesc: '添加規則對種子名或者文件名進行預處理以校正識別',
identifiersPlaceholder: '支持正則表達式,特殊字符需要\\轉義,一行為一組', identifiersPlaceholder: '支持正則表達式,特殊字符需要\\轉義,一行為一組',
identifiersHint: '支持正則表達式,特殊字符需要\\轉義,一行為一組', identifiersHint: '支持正則表達式,特殊字符需要\\轉義,一行為一組',
formatTitle: '支持的配置格式(注意空格):', formatTitle: '支持的配置格式(注意空格):',
@@ -1216,7 +1231,7 @@ export default {
}, },
search: { search: {
basicSettings: '基礎設置', basicSettings: '基礎設置',
basicSettingsDesc: '設定數據源、規則組等基礎信息', basicSettingsDesc: '設定數據源、規則組等基礎信息',
recognizeSource: '識別數據源', recognizeSource: '識別數據源',
recognizeSourceDesc: '默認使用TMDB。豆瓣識別中文作品通常更友好但有些國外作品信息不完整。', recognizeSourceDesc: '默認使用TMDB。豆瓣識別中文作品通常更友好但有些國外作品信息不完整。',
themoviedb: 'TheMovieDb', themoviedb: 'TheMovieDb',
@@ -1252,7 +1267,7 @@ export default {
}, },
directory: { directory: {
storage: '存儲', storage: '存儲',
storageDesc: '設置本地或網盤存儲', storageDesc: '設置本地或網盤存儲',
directory: '目錄', directory: '目錄',
directoryDesc: '設置媒體文件整理目錄結構,按先後順序依次匹配。', directoryDesc: '設置媒體文件整理目錄結構,按先後順序依次匹配。',
organizeAndScrap: '整理 & 刮削', organizeAndScrap: '整理 & 刮削',
@@ -1313,7 +1328,7 @@ export default {
}, },
scheduler: { scheduler: {
scheduledTasks: '定時作業', scheduledTasks: '定時作業',
scheduledTasksDesc: '包含系統內置服務以及插件提供的服務,手動執行不會影響作業正常的時間表。', scheduledTasksDesc: '包含系統內置服務以及插件提供的服務',
provider: '提供者', provider: '提供者',
taskName: '任務名稱', taskName: '任務名稱',
taskStatus: '任務狀態', taskStatus: '任務狀態',
@@ -1360,6 +1375,56 @@ export default {
settingsSaveSuccess: '訂閱基礎設置保存成功', settingsSaveSuccess: '訂閱基礎設置保存成功',
settingsSaveFailed: '訂閱基礎設置保存失敗!', settingsSaveFailed: '訂閱基礎設置保存失敗!',
}, },
cache: {
title: '緩存',
description: '種子緩存、圖片文件緩存管理',
subtitle: '管理緩存的站點資源',
filterByTitle: '按標題篩選',
filterBySite: '按站點篩選',
selectSite: '選擇站點',
refresh: '刷新緩存',
deleteSelected: '刪除選中',
clearAll: '清空緩存',
refreshSuccess: '緩存刷新完成',
refreshFailed: '刷新緩存失敗',
clearSuccess: '緩存清理完成',
clearFailed: '清理緩存失敗',
deleteSuccess: '緩存項刪除成功',
deleteFailed: '刪除緩存項失敗',
deleteSelectedSuccess: '成功刪除 {count} 個緩存項',
deleteSelectedFailed: '刪除緩存項失敗',
loadFailed: '加載緩存數據失敗',
selectDeleteWarning: '請選擇要刪除的緩存項',
reidentify: '重新識別',
reidentifySuccess: '重新識別完成',
reidentifyFailed: '重新識別失敗',
poster: '海報',
torrentTitle: '標題',
site: '站點',
size: '大小',
publishTime: '發布時間',
recognitionResult: '識別結果',
actions: '操作',
unrecognized: '未識別',
noData: '暫無緩存數據',
noDataHint: '點擊"刷新緩存"按鈕獲取最新的種子緩存',
reidentifyDialog: {
title: '重新識別',
torrentInfo: '種子信息',
tmdbId: 'TMDB ID',
tmdbIdHint: '可選手動指定TMDB ID進行識別',
doubanId: '豆瓣 ID',
doubanIdHint: '可選手動指定豆瓣ID進行識別',
autoHint: '如果不指定ID將自動重新識別該種子',
cancel: '取消',
confirm: '重新識別',
},
mediaType: {
movie: '電影',
tv: '電視劇',
},
clearConfirm: '確認清空所有緩存嗎?',
},
}, },
dialog: { dialog: {
progress: { progress: {
@@ -1681,8 +1746,8 @@ export default {
previous: '上一步', previous: '上一步',
confirm: '確認', confirm: '確認',
manualTitle: '手動整理', manualTitle: '手動整理',
multipleItemsTitle: '整理 - 共 {count} 項', multipleItemsTitle: '共 {count} 項',
singleItemTitle: '整理 - {path}', singleItemTitle: '{path}',
targetStorage: '目的存儲', targetStorage: '目的存儲',
targetStorageHint: '整理目的存儲', targetStorageHint: '整理目的存儲',
transferType: '整理方式', transferType: '整理方式',
@@ -1731,7 +1796,7 @@ export default {
}, },
subscribeEdit: { subscribeEdit: {
titleDefault: '默認訂閱規則', titleDefault: '默認訂閱規則',
titleEditFormat: '編輯訂閱 - {name} {season}', titleEdit: '編輯訂閱',
seasonFormat: '第 {number} 季', seasonFormat: '第 {number} 季',
tabs: { tabs: {
basic: '基礎', basic: '基礎',
@@ -1990,6 +2055,38 @@ export default {
folderName: '文件夾名稱', folderName: '文件夾名稱',
cancel: '取消', cancel: '取消',
create: '創建', create: '創建',
clone: '分身',
cloneTitle: '創建插件分身',
cloneSubtitle: '為 {name} 創建獨立的分身實例',
cloneFeature: '插件分身功能',
cloneDescription: '創建插件的獨立副本,擁有獨立的配置和數據,適用於多賬號、測試環境等場景',
suffix: '分身後綴',
suffixPlaceholder: '例如Test、Backup、Site1',
suffixHint: '用於區分分身的唯一標識,只能包含英文字母和數字',
suffixRequired: '分身後綴不能為空',
suffixFormatError: '只能包含英文字母和數字',
suffixLengthError: '長度不能超過20個字符',
cloneName: '分身名稱',
cloneNamePlaceholder: '例如:自動備份 測試版',
cloneNameHint: '分身插件的顯示名稱(可選)',
cloneDefaultName: '{name} 分身',
cloneDescriptionLabel: '分身描述',
cloneDescriptionPlaceholder: '描述這個分身的用途和特點...',
cloneDescriptionHint: '詳細描述分身插件的用途(可選)',
cloneDefaultDescription: '{description} (分身版本)',
cloneVersion: '版本號',
cloneVersionPlaceholder: '例如1.0、2.1.0',
cloneVersionHint: '自定義分身插件的版本號(可選)',
cloneIcon: '圖標URL',
cloneIconPlaceholder: 'https://example.com/icon.png',
cloneIconHint: '自定義分身插件的圖標(可選)',
cloneNotice: '分身插件創建後默認為禁用狀態,需要手動配置啟用。分身後綴一旦確定無法修改。',
createClone: '創建分身',
cloning: '正在創建 {name} 的分身...',
cloneSuccess: '插件分身 {name} 創建成功!',
cloneFailed: '插件分身創建失敗:{message}',
cloneFailedGeneral: '插件分身創建失敗',
logTitle: '插件日誌',
}, },
profile: { profile: {
personalInfo: '個人信息', personalInfo: '個人信息',

View File

@@ -1,24 +1,28 @@
<script setup lang="ts"> <script setup lang="ts">
import PersonCardListView from '@/views/discover/PersonCardListView.vue' import PersonCardListView from '@/views/discover/PersonCardListView.vue'
// 输入参数
const props = defineProps({
// API路径
paths: Array as PropType<string[]> | PropType<string>,
})
// 路由参数 // 路由参数
const route = useRoute() const route = useRoute()
const id = route.query?.id?.toString()
const title = route.query?.title?.toString() // 标题
const source = route.query?.source?.toString() let title = route.query?.title?.toString()
const type = route.query?.type?.toString()
const apipath = route.query?.apipath?.toString() // 计算API路径
function getApiPath(paths: string[] | string) {
if (Array.isArray(paths)) return paths.join('/')
else return paths
}
</script> </script>
<template> <template>
<div> <div>
<VPageContentTitle :title="title" /> <VPageContentTitle :title="title" />
<PersonCardListView <PersonCardListView :apipath="getApiPath(props.paths || '')" />
:credits-id="id"
:credits-name="title"
:credits-source="source"
:credits-type="type"
:credits-apipath="apipath"
/>
</div> </div>
</template> </template>

View File

@@ -14,12 +14,6 @@ const mediaid = route.query?.mediaid?.toString()
// 类型:电影、电视剧 // 类型:电影、电视剧
const type = route.query?.type?.toString() const type = route.query?.type?.toString()
// 媒体信息来源TMDB、豆瓣
const source = route.query?.source?.toString() || 'themoviedb'
// TMDB ID
const page = route.query?.page?.toString() || '1'
// 标题 // 标题
const title = route.query?.title?.toString() const title = route.query?.title?.toString()
@@ -29,6 +23,6 @@ const year = route.query?.year?.toString()
<template> <template>
<div> <div>
<MediaDetailView :mediaid="mediaid" :type="type" :source="source" :page="page" :title="title" :year="year" /> <MediaDetailView :mediaid="mediaid" :type="type" :title="title" :year="year" />
</div> </div>
</template> </template>

View File

@@ -8,9 +8,10 @@ import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue' import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue' import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue' import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
import AccountSettingScheduler from '@/views/setting/AccountSettingScheduler.vue' import AccountSettingService from '@/views/setting/AccountSettingService.vue'
import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue' import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue'
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue' import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
import AccountSettingCache from '@/views/setting/AccountSettingCache.vue'
import { getSettingTabs } from '@/router/i18n-menu' import { getSettingTabs } from '@/router/i18n-menu'
const route = useRoute() const route = useRoute()
@@ -81,7 +82,16 @@ const settingTabs = computed(() => getSettingTabs())
<VWindowItem value="scheduler"> <VWindowItem value="scheduler">
<transition name="fade-slide" appear> <transition name="fade-slide" appear>
<div> <div>
<AccountSettingScheduler /> <AccountSettingService />
</div>
</transition>
</VWindowItem>
<!-- 缓存 -->
<VWindowItem value="cache">
<transition name="fade-slide" appear>
<div>
<AccountSettingCache />
</div> </div>
</transition> </transition>
</VWindowItem> </VWindowItem>

View File

@@ -170,6 +170,12 @@ export function getSettingTabs() {
tab: 'scheduler', tab: 'scheduler',
description: t('settingTabs.scheduler.description'), description: t('settingTabs.scheduler.description'),
}, },
{
title: t('settingTabs.cache.title'),
icon: 'mdi-database',
tab: 'cache',
description: t('settingTabs.cache.description'),
},
{ {
title: t('settingTabs.notification.title'), title: t('settingTabs.notification.title'),
icon: 'mdi-bell', icon: 'mdi-bell',

View File

@@ -113,7 +113,7 @@ async function fetchData({ done }: { done: any }) {
<template> <template>
<LoadingBanner v-if="!isRefreshed" class="mt-12" /> <LoadingBanner v-if="!isRefreshed" class="mt-12" />
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible" @load="fetchData"> <VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible px-3" @load="fetchData">
<template #loading /> <template #loading />
<template #empty /> <template #empty />
<div v-if="dataList.length > 0" class="grid gap-4 grid-media-card" tabindex="0"> <div v-if="dataList.length > 0" class="grid gap-4 grid-media-card" tabindex="0">

View File

@@ -114,7 +114,7 @@ onBeforeMount(() => {
'ring-1 ring-gray-700': isImageLoaded, 'ring-1 ring-gray-700': isImageLoaded,
}" }"
> >
<VImg v-img :src="getPersonImage()" cover @load="isImageLoaded = true" /> <VImg :src="getPersonImage()" cover @load="isImageLoaded = true" />
</VAvatar> </VAvatar>
<div class="ms-3"> <div class="ms-3">
<h1 class="text-3xl lg:text-4xl text-center text-lg-left"> <h1 class="text-3xl lg:text-4xl text-center text-lg-left">

View File

@@ -5,7 +5,6 @@ import api from '@/api'
import type { Plugin } from '@/api/types' import type { Plugin } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue' import NoDataFound from '@/components/NoDataFound.vue'
import PluginAppCard from '@/components/cards/PluginAppCard.vue' import PluginAppCard from '@/components/cards/PluginAppCard.vue'
import PluginCard from '@/components/cards/PluginCard.vue'
import noImage from '@images/logos/plugin.png' import noImage from '@images/logos/plugin.png'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import { isNullOrEmptyObject } from '@/@core/utils' import { isNullOrEmptyObject } from '@/@core/utils'
@@ -13,7 +12,7 @@ import { getPluginTabs } from '@/router/i18n-menu'
import PluginMarketSettingDialog from '@/components/dialog/PluginMarketSettingDialog.vue' import PluginMarketSettingDialog from '@/components/dialog/PluginMarketSettingDialog.vue'
import { useDynamicButton } from '@/composables/useDynamicButton' import { useDynamicButton } from '@/composables/useDynamicButton'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import PluginFolderCard from '@/components/cards/PluginFolderCard.vue' import PluginMixedSortCard from '@/components/cards/PluginMixedSortCard.vue'
// 国际化 // 国际化
const { t } = useI18n() const { t } = useI18n()
@@ -39,7 +38,7 @@ const pluginId = ref(route.query.id)
const activeSort = ref(null) const activeSort = ref(null)
// 插件顺序配置 // 插件顺序配置
const orderConfig = ref<{ id: string }[]>([]) const orderConfig = ref<{ id: string; type?: string; order?: number }[]>([])
// 排序选项 // 排序选项
const sortOptions = computed(() => [ const sortOptions = computed(() => [
@@ -176,6 +175,33 @@ const newFolderDialog = ref(false)
// 新文件夹名称 // 新文件夹名称
const newFolderName = ref('') const newFolderName = ref('')
// 获取文件夹内筛选后的插件
const getFilteredFolderPlugins = (folderName: string) => {
const folderData = pluginFolders.value[folderName]
const folderPluginIds = Array.isArray(folderData) ? folderData : folderData?.plugins || []
// 获取文件夹内的插件并应用筛选条件
const folderPlugins: Plugin[] = []
folderPluginIds.forEach((pluginId: string) => {
const plugin = dataList.value.find(p => p.id === pluginId)
if (plugin) {
folderPlugins.push(plugin)
}
})
// 应用筛选条件
return folderPlugins.filter(plugin => {
if (!installedFilter.value && !hasUpdateFilter.value) return true
if (hasUpdateFilter.value) {
return plugin.has_update
}
if (installedFilter.value) {
return plugin.plugin_name?.toLowerCase().includes((installedFilter.value as string).toLowerCase())
}
return true
})
}
// 显示的插件列表(考虑文件夹筛选) // 显示的插件列表(考虑文件夹筛选)
const displayedPlugins = computed(() => { const displayedPlugins = computed(() => {
if (!currentFolder.value) { if (!currentFolder.value) {
@@ -187,15 +213,21 @@ const displayedPlugins = computed(() => {
}) })
return filteredDataList.value.filter(plugin => !folderedPluginIds.has(plugin.id)) return filteredDataList.value.filter(plugin => !folderedPluginIds.has(plugin.id))
} else { } else {
// 文件夹内:只显示文件夹中的插件 // 文件夹内:返回筛选后的插件
const folderData = pluginFolders.value[currentFolder.value] return getFilteredFolderPlugins(currentFolder.value)
const folderPluginIds = Array.isArray(folderData) ? folderData : folderData?.plugins || []
return filteredDataList.value.filter(plugin => folderPluginIds.includes(plugin.id))
} }
}) })
// 可拖拽的插件列表(主列表用) // 混合排序项目类型
const draggableMainPlugins = ref<Plugin[]>([]) interface MixedSortItem {
type: 'folder' | 'plugin'
id: string
data: any
order: number
}
// 混合排序列表(包含文件夹和插件)
const mixedSortList = ref<MixedSortItem[]>([])
// 可拖拽的插件列表(文件夹内用) // 可拖拽的插件列表(文件夹内用)
const draggableFolderPlugins = ref<Plugin[]>([]) const draggableFolderPlugins = ref<Plugin[]>([])
@@ -203,30 +235,6 @@ const draggableFolderPlugins = ref<Plugin[]>([])
// 是否正在拖拽排序中 // 是否正在拖拽排序中
const isDraggingSortMode = ref(false) const isDraggingSortMode = ref(false)
// 监听displayedPlugins变化更新可拖拽列表避免拖拽时的循环更新
watch(
displayedPlugins,
newPlugins => {
if (isDraggingSortMode.value) return // 拖拽排序时跳过更新
if (!currentFolder.value) {
draggableMainPlugins.value = [...newPlugins]
} else {
draggableFolderPlugins.value = [...newPlugins]
}
},
{ immediate: true },
)
// 监听文件夹切换,更新可拖拽列表
watch(currentFolder, () => {
if (!currentFolder.value) {
draggableMainPlugins.value = [...displayedPlugins.value]
} else {
draggableFolderPlugins.value = [...displayedPlugins.value]
}
})
// 显示的文件夹列表(按排序显示) // 显示的文件夹列表(按排序显示)
const displayedFolders = computed(() => { const displayedFolders = computed(() => {
if (currentFolder.value) return [] // 在文件夹内不显示其他文件夹 if (currentFolder.value) return [] // 在文件夹内不显示其他文件夹
@@ -239,17 +247,108 @@ const displayedFolders = computed(() => {
const unsortedFolders = folderNames.filter(name => !folderOrder.value.includes(name)) const unsortedFolders = folderNames.filter(name => !folderOrder.value.includes(name))
sortedFolderNames.push(...unsortedFolders) sortedFolderNames.push(...unsortedFolders)
return sortedFolderNames.map(folderName => { return sortedFolderNames
const folderData = pluginFolders.value[folderName] .map(folderName => {
const plugins = Array.isArray(folderData) ? folderData : folderData?.plugins || [] const folderData = pluginFolders.value[folderName]
const config = Array.isArray(folderData) ? {} : folderData const config = Array.isArray(folderData) ? {} : folderData
return { // 获取筛选后的插件数量
name: folderName, const filteredPlugins = getFilteredFolderPlugins(folderName)
pluginCount: plugins.length,
config: config, return {
name: folderName,
pluginCount: filteredPlugins.length,
config: config,
}
})
.filter(folder => {
// 当有筛选条件时,只显示包含筛选后插件的文件夹
if (installedFilter.value || hasUpdateFilter.value) {
return folder.pluginCount > 0
}
return true
})
})
// 更新混合排序列表
function updateMixedSortList() {
if (isDraggingSortMode.value) return // 拖拽排序时跳过更新
if (!currentFolder.value) {
// 主列表:创建混合列表
const items: MixedSortItem[] = []
// 创建统一的排序索引
let globalOrder = 0
// 始终使用全局排序配置来创建混合列表
const allItems: { type: 'folder' | 'plugin'; id: string; data: any; order: number }[] = []
// 添加文件夹项目
displayedFolders.value.forEach(folder => {
const orderItem = orderConfig.value.find((item: any) => item.type === 'folder' && item.id === folder.name)
allItems.push({
type: 'folder',
id: folder.name,
data: folder,
order: orderItem?.order ?? 999,
})
})
// 添加插件项目
displayedPlugins.value.forEach(plugin => {
const orderItem = orderConfig.value.find((item: any) => item.type === 'plugin' && item.id === plugin.id)
allItems.push({
type: 'plugin',
id: plugin.id || '',
data: plugin,
order: orderItem?.order ?? 999,
})
})
// 按order排序
allItems.sort((a, b) => a.order - b.order)
// 转换为MixedSortItem格式
allItems.forEach((item, index) => {
items.push({
type: item.type,
id: item.id,
data: item.data,
order: index,
})
})
// 按order排序
items.sort((a, b) => a.order - b.order)
mixedSortList.value = items
} else {
// 文件夹内:只更新插件列表
draggableFolderPlugins.value = [...displayedPlugins.value]
}
}
// 监听相关数据变化,更新混合排序列表
watch(
[displayedPlugins, displayedFolders, orderConfig, folderOrder, installedFilter, hasUpdateFilter],
() => {
// 只有在非拖拽状态下才更新
if (!isDraggingSortMode.value) {
updateMixedSortList()
} }
}) },
{
immediate: true,
deep: true,
},
)
// 监听文件夹切换,更新列表
watch(currentFolder, () => {
// 只有在非拖拽状态下才更新
if (!isDraggingSortMode.value) {
updateMixedSortList()
}
}) })
// 加载插件顺序 // 加载插件顺序
@@ -257,11 +356,33 @@ async function loadPluginOrderConfig() {
// 顺序配置 // 顺序配置
const local_order = localStorage.getItem('MP_PLUGIN_ORDER') const local_order = localStorage.getItem('MP_PLUGIN_ORDER')
if (local_order) { if (local_order) {
orderConfig.value = JSON.parse(local_order) const parsed = JSON.parse(local_order)
// 兼容旧格式只有id和新格式包含type和order
if (parsed.length > 0 && typeof parsed[0] === 'object' && 'type' in parsed[0]) {
orderConfig.value = parsed
} else {
// 旧格式,转换为新格式
orderConfig.value = parsed.map((item: any, index: number) => ({
id: typeof item === 'string' ? item : item.id,
type: 'plugin',
order: index,
}))
}
} else { } else {
const response2 = await api.get('/user/config/PluginOrder') const response2 = await api.get('/user/config/PluginOrder')
if (response2 && response2.data && response2.data.value) { if (response2 && response2.data && response2.data.value) {
orderConfig.value = response2.data.value const serverData = response2.data.value
// 兼容服务端的旧格式和新格式
if (serverData.length > 0 && typeof serverData[0] === 'object' && 'type' in serverData[0]) {
orderConfig.value = serverData
} else {
// 旧格式,转换为新格式
orderConfig.value = serverData.map((item: any, index: number) => ({
id: typeof item === 'string' ? item : item.id,
type: 'plugin',
order: index,
}))
}
localStorage.setItem('MP_PLUGIN_ORDER', JSON.stringify(orderConfig.value)) localStorage.setItem('MP_PLUGIN_ORDER', JSON.stringify(orderConfig.value))
} }
} }
@@ -282,56 +403,75 @@ function sortPluginOrder() {
}) })
} }
// 保存顺序设置 // 保存混合排序
async function savePluginOrder() { async function saveMixedSortOrder() {
// 只在主列表中保存顺序,文件夹内不保存全局顺序
if (currentFolder.value) return
// 顺序配置
const orderObj = filteredDataList.value.map(item => ({ id: item.id || '' }))
orderConfig.value = orderObj
const orderString = JSON.stringify(orderObj)
localStorage.setItem('MP_PLUGIN_ORDER', orderString)
// 保存到服务端
try { try {
await api.post('/user/config/PluginOrder', orderObj) // 分离文件夹和插件,并记录它们的全局排序位置
} catch (error) { const newFolderOrder: string[] = []
console.error(error) const newPluginOrder: Plugin[] = []
} const globalOrder: { type: 'folder' | 'plugin'; id: string; order: number }[] = []
}
// 保存主列表插件顺序 mixedSortList.value.forEach((item, index) => {
async function saveMainPluginOrder() { globalOrder.push({
try { type: item.type,
// 更新主列表数据 id: item.id,
const newOrderedList = [...draggableMainPlugins.value] order: index,
})
// 添加文件夹中的插件到末尾 if (item.type === 'folder') {
newFolderOrder.push(item.id)
} else if (item.type === 'plugin') {
newPluginOrder.push(item.data)
}
})
// 更新文件夹排序并设置order属性
folderOrder.value = newFolderOrder
newFolderOrder.forEach((folderName, index) => {
if (pluginFolders.value[folderName]) {
// 找到该文件夹在全局排序中的位置
const globalOrderItem = globalOrder.find(item => item.type === 'folder' && item.id === folderName)
pluginFolders.value[folderName].order = globalOrderItem ? globalOrderItem.order : index
}
})
// 添加文件夹中的插件到插件列表末尾
Object.values(pluginFolders.value).forEach(folderData => { Object.values(pluginFolders.value).forEach(folderData => {
const plugins = Array.isArray(folderData) ? folderData : folderData.plugins || [] const plugins = Array.isArray(folderData) ? folderData : folderData.plugins || []
plugins.forEach((id: string) => { plugins.forEach((id: string) => {
const folderPlugin = dataList.value.find(p => p.id === id) const folderPlugin = dataList.value.find(p => p.id === id)
if (folderPlugin && !newOrderedList.find(p => p.id === id)) { if (folderPlugin && !newPluginOrder.find(p => p.id === id)) {
newOrderedList.push(folderPlugin) newPluginOrder.push(folderPlugin)
} }
}) })
}) })
filteredDataList.value = newOrderedList // 更新插件列表
filteredDataList.value = newPluginOrder
// 保存排序配置 // 保存插件排序配置(包含全局排序信息)
const orderObj = newOrderedList.map(item => ({ id: item.id || '' })) const orderObj = globalOrder.map(item => ({
id: item.id,
type: item.type,
order: item.order,
}))
orderConfig.value = orderObj orderConfig.value = orderObj
const orderString = JSON.stringify(orderObj) const orderString = JSON.stringify(orderObj)
localStorage.setItem('MP_PLUGIN_ORDER', orderString) localStorage.setItem('MP_PLUGIN_ORDER', orderString)
// 保存到服务端 // 保存到服务端
await api.post('/user/config/PluginOrder', orderObj) await api.post('/user/config/PluginOrder', orderObj)
// 保存文件夹排序
await savePluginFolders()
} catch (error) { } catch (error) {
console.error(error)
} finally { } finally {
// 清除拖拽标志 // 清除拖拽标志
isDraggingSortMode.value = false isDraggingSortMode.value = false
// 在清除拖拽标志后更新混合排序列表显示
updateMixedSortList()
} }
} }
@@ -353,11 +493,36 @@ async function saveFolderPluginOrder() {
folderData.plugins = newPluginIds folderData.plugins = newPluginIds
} }
// 更新全局排序配置中文件夹内插件的顺序
const folderOrderItem = orderConfig.value.find(
(item: any) => item.type === 'folder' && item.id === currentFolder.value,
)
const folderGlobalOrder = folderOrderItem?.order ?? 999
// 为文件夹内的插件分配连续的order值
newPluginIds.forEach((pluginId, index) => {
const existingItem = orderConfig.value.find((item: any) => item.type === 'plugin' && item.id === pluginId)
if (existingItem) {
existingItem.order = folderGlobalOrder + 0.1 + index * 0.01 // 使用小数确保在文件夹后面
} else {
orderConfig.value.push({
id: pluginId,
type: 'plugin',
order: folderGlobalOrder + 0.1 + index * 0.01,
})
}
})
// 保存全局排序配置
const orderString = JSON.stringify(orderConfig.value)
localStorage.setItem('MP_PLUGIN_ORDER', orderString)
await api.post('/user/config/PluginOrder', orderConfig.value)
// 保存到后端 // 保存到后端
await savePluginFolders() await savePluginFolders()
} }
} catch (error) { } catch (error) {
console.error('保存文件夹内排序失败:', error) console.error(error)
} finally { } finally {
// 清除拖拽标志 // 清除拖拽标志
isDraggingSortMode.value = false isDraggingSortMode.value = false
@@ -523,6 +688,8 @@ async function getPluginStatistics() {
async function refreshData() { async function refreshData() {
await fetchInstalledPlugins() await fetchInstalledPlugins()
fetchUninstalledPlugins() fetchUninstalledPlugins()
// 重新加载文件夹配置,确保分身插件能正确显示在文件夹中
await loadPluginFolders()
} }
// 对uninstalledList进行排序到sortedUninstalledList // 对uninstalledList进行排序到sortedUninstalledList
@@ -680,10 +847,18 @@ async function loadPluginFolders() {
pluginFolders.value = processedFolders pluginFolders.value = processedFolders
// 设置文件夹排序 // 设置文件夹排序 - 使用全局排序配置
folderOrder.value = Object.keys(processedFolders).sort( const folderNames = Object.keys(processedFolders)
(a, b) => (processedFolders[a].order || 0) - (processedFolders[b].order || 0), folderOrder.value = folderNames.sort((a, b) => {
) // 从全局排序配置中查找文件夹的order
const aOrderItem = orderConfig.value.find((item: any) => item.type === 'folder' && item.id === a)
const bOrderItem = orderConfig.value.find((item: any) => item.type === 'folder' && item.id === b)
const aOrder = aOrderItem?.order ?? processedFolders[a].order ?? 999
const bOrder = bOrderItem?.order ?? processedFolders[b].order ?? 999
return aOrder - bOrder
})
} catch (error) { } catch (error) {
pluginFolders.value = {} pluginFolders.value = {}
folderOrder.value = [] folderOrder.value = []
@@ -791,7 +966,7 @@ async function renameFolder(oldName: string, newName: string) {
$toast.success(t('plugin.folderRenameSuccess')) $toast.success(t('plugin.folderRenameSuccess'))
} catch (error) { } catch (error) {
console.error('重命名文件夹失败:', error) console.error(error)
// 回滚本地更改 // 回滚本地更改
pluginFolders.value[oldName] = pluginFolders.value[newName] || { plugins: [] } pluginFolders.value[oldName] = pluginFolders.value[newName] || { plugins: [] }
delete pluginFolders.value[newName] delete pluginFolders.value[newName]
@@ -885,33 +1060,10 @@ async function updateFolderConfig(folderName: string, config: any) {
} }
} }
// 文件夹拖拽排序结束事件
function onFolderSortEnd() {
// 保存新的文件夹顺序
savePluginFolders()
}
// 当前拖拽的插件ID // 当前拖拽的插件ID
const currentDraggedPluginId = ref('') const currentDraggedPluginId = ref('')
// 处理拖拽到文件夹的事件 // 处理拖拽到文件夹的事件
function handleDragOver(event: DragEvent) {
event.preventDefault()
event.dataTransfer!.dropEffect = 'move'
const target = event.currentTarget as HTMLElement
target.classList.add('drag-over')
}
function handleDragEnter(event: DragEvent) {
event.preventDefault()
}
function handleDragLeave(event: DragEvent) {
event.preventDefault()
const target = event.currentTarget as HTMLElement
target.classList.remove('drag-over')
}
async function handleDropToFolder(event: DragEvent, folderName: string) { async function handleDropToFolder(event: DragEvent, folderName: string) {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
@@ -964,9 +1116,9 @@ async function handleDropToFolder(event: DragEvent, folderName: string) {
}) })
// 从主列表中移除(如果存在) // 从主列表中移除(如果存在)
const mainIndex = draggableMainPlugins.value.findIndex(p => p.id === pluginId) const mainIndex = mixedSortList.value.findIndex(item => item.type === 'plugin' && item.id === pluginId)
if (mainIndex > -1) { if (mainIndex > -1) {
draggableMainPlugins.value.splice(mainIndex, 1) mixedSortList.value.splice(mainIndex, 1)
} }
// 添加到目标文件夹 // 添加到目标文件夹
@@ -993,9 +1145,11 @@ async function handleDropToFolder(event: DragEvent, folderName: string) {
// 保存配置 // 保存配置
await savePluginFolders() await savePluginFolders()
// 更新混合排序列表
updateMixedSortList()
$toast.success(`插件已移动到文件夹 "${folderName}"`) $toast.success(`插件已移动到文件夹 "${folderName}"`)
} catch (error) { } catch (error) {
console.error('拖拽到文件夹失败:', error)
$toast.error('操作失败') $toast.error('操作失败')
} }
} }
@@ -1008,10 +1162,18 @@ function onDragStartPlugin(evt: any) {
// 从oldIndex获取插件ID // 从oldIndex获取插件ID
const oldIndex = evt.oldIndex const oldIndex = evt.oldIndex
if (oldIndex !== undefined) { if (oldIndex !== undefined) {
const plugin = currentFolder.value ? draggableFolderPlugins.value[oldIndex] : draggableMainPlugins.value[oldIndex] if (currentFolder.value) {
if (plugin && plugin.id) { const plugin = draggableFolderPlugins.value[oldIndex]
currentDraggedPluginId.value = plugin.id if (plugin && plugin.id) {
return currentDraggedPluginId.value = plugin.id
return
}
} else {
const item = mixedSortList.value[oldIndex]
if (item && item.id) {
currentDraggedPluginId.value = item.id
return
}
} }
} }
@@ -1034,13 +1196,6 @@ function onDragStartPlugin(evt: any) {
currentDraggedPluginId.value = item.getAttribute('data-plugin-id') currentDraggedPluginId.value = item.getAttribute('data-plugin-id')
} }
} }
// 拖拽结束事件
function onDragEndPlugin(evt: any) {
currentDraggedPluginId.value = ''
// 清除拖拽标志
isDraggingSortMode.value = false
}
</script> </script>
<template> <template>
@@ -1207,66 +1362,37 @@ function onDragEndPlugin(evt: any) {
<LoadingBanner v-if="!isRefreshed" class="mt-12" /> <LoadingBanner v-if="!isRefreshed" class="mt-12" />
<!-- 文件夹和插件网格 --> <!-- 文件夹和插件网格 -->
<div v-if="displayedFolders.length > 0 || displayedPlugins.length > 0" class="grid gap-4 grid-plugin-card"> <div v-if="(mixedSortList.length > 0 || displayedPlugins.length > 0) && isRefreshed">
<!-- 文件夹卡片 - 使用draggable进行排序 --> <!-- 混合排序列表文件夹和插件 -->
<draggable
v-if="displayedFolders.length > 0 && isRefreshed"
v-model="folderOrder"
@end="onFolderSortEnd"
handle=".cursor-move"
item-key="name"
tag="div"
:component-data="{ style: 'display: contents;' }"
:disabled="currentFolder !== ''"
group="folders"
>
<template #item="{ element: folderName }">
<div
v-if="displayedFolders.find(f => f.name === folderName)"
class="drop-zone"
@dragover="handleDragOver($event)"
@dragenter="handleDragEnter($event)"
@dragleave="handleDragLeave($event)"
@drop="handleDropToFolder($event, folderName)"
>
<PluginFolderCard
:folder-name="folderName"
:plugin-count="displayedFolders.find(f => f.name === folderName)?.pluginCount || 0"
:folder-config="displayedFolders.find(f => f.name === folderName)?.config || {}"
@open="openFolder"
@delete="deleteFolder"
@rename="renameFolder"
@update-config="updateFolderConfig"
/>
</div>
</template>
</draggable>
<!-- 插件卡片 -->
<template v-if="!currentFolder"> <template v-if="!currentFolder">
<!-- 主列表使用draggable进行排序 --> <!-- 主列表使用draggable进行混合排序 -->
<draggable <draggable
v-model="draggableMainPlugins" v-model="mixedSortList"
@end="saveMainPluginOrder" @end="saveMixedSortOrder"
@start="onDragStartPlugin" @start="onDragStartPlugin"
@sort="onDragEndPlugin"
handle=".cursor-move" handle=".cursor-move"
item-key="id" item-key="id"
tag="div" tag="div"
:component-data="{ style: 'display: contents;' }" class="grid gap-4 grid-plugin-card"
group="plugins" group="mixed"
> >
<template #item="{ element }"> <template #item="{ element }">
<div class="plugin-item-wrapper" :data-plugin-id="element.id"> <PluginMixedSortCard
<PluginCard :item="element"
:count="PluginStatistics[element.id || '0']" :plugin-statistics="PluginStatistics"
:plugin="element" :plugin-actions="pluginActions"
:action="pluginActions[element.id || '0']" @open-folder="openFolder"
@remove="refreshData" @delete-folder="deleteFolder"
@save="refreshData" @rename-folder="(oldName, newName) => renameFolder(oldName, newName)"
@action-done="pluginActions[element.id || '0'] = false" @update-folder-config="(folderName, config) => updateFolderConfig(folderName, config)"
/> @refresh-data="refreshData"
</div> @action-done="
pluginId => {
pluginActions[pluginId] = false
}
"
@drop-to-folder="(event, folderName) => handleDropToFolder(event, folderName)"
/>
</template> </template>
</draggable> </draggable>
</template> </template>
@@ -1280,29 +1406,23 @@ function onDragEndPlugin(evt: any) {
handle=".cursor-move" handle=".cursor-move"
item-key="id" item-key="id"
tag="div" tag="div"
:component-data="{ style: 'display: contents;' }" class="grid gap-4 grid-plugin-card"
group="plugins" group="plugins"
> >
<template #item="{ element }"> <template #item="{ element }">
<div class="plugin-item-wrapper" :data-plugin-id="element.id"> <PluginMixedSortCard
<PluginCard :item="{ type: 'plugin', id: element.id, data: element, order: 0 }"
:count="PluginStatistics[element.id || '0']" :plugin-statistics="PluginStatistics"
:plugin="element" :plugin-actions="pluginActions"
:action="pluginActions[element.id || '0']" :show-remove-button="true"
@remove="refreshData" @refresh-data="refreshData"
@save="refreshData" @action-done="
@action-done="pluginActions[element.id || '0'] = false" pluginId => {
/> pluginActions[pluginId] = false
<!-- 移出文件夹按钮 --> }
<VBtn "
icon="mdi-folder-remove" @remove-from-folder="removeFromFolder"
variant="text" />
color="warning"
size="small"
class="remove-from-folder-btn"
@click="removeFromFolder(element.id || '')"
/>
</div>
</template> </template>
</draggable> </draggable>
</template> </template>
@@ -1402,7 +1522,7 @@ function onDragEndPlugin(evt: any) {
/> />
</VToolbar> </VToolbar>
<VDialogCloseBtn @click="closeSearchDialog" /> <VDialogCloseBtn @click="closeSearchDialog" />
<VList v-if="filterPlugins.length > 0" lines="three"> <VList v-if="filterPlugins.length > 0" lines="two">
<VVirtualScroll :items="filterPlugins"> <VVirtualScroll :items="filterPlugins">
<template #default="{ item }"> <template #default="{ item }">
<VListItem @click="openPlugin(item)"> <VListItem @click="openPlugin(item)">
@@ -1440,6 +1560,7 @@ function onDragEndPlugin(evt: any) {
</VList> </VList>
</VCard> </VCard>
</VDialog> </VDialog>
<!-- 安装插件进度框 --> <!-- 安装插件进度框 -->
<VDialog v-if="progressDialog" v-model="progressDialog" :scrim="false" width="25rem"> <VDialog v-if="progressDialog" v-model="progressDialog" :scrim="false" width="25rem">
<VCard color="primary"> <VCard color="primary">
@@ -1477,35 +1598,5 @@ function onDragEndPlugin(evt: any) {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
// 拖拽相关样式 // 样式已移至 PluginMixedSortCard 组件
.drop-zone {
transition: all 0.3s ease;
&.drag-over {
transform: scale(1.02);
box-shadow: 0 0 20px rgba(33, 150, 243, 0.5);
border: 2px dashed #2196f3;
border-radius: 16px;
}
}
.plugin-item-wrapper {
position: relative;
.remove-from-folder-btn {
position: absolute;
top: 4px;
right: 4px;
z-index: 10;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(4px);
border-radius: 50%;
opacity: 0;
transition: opacity 0.3s ease;
}
&:hover .remove-from-folder-btn {
opacity: 1;
}
}
</style> </style>

View File

@@ -11,7 +11,6 @@ import router from '@/router'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import { formatFileSize } from '@/@core/utils/formatters' import { formatFileSize } from '@/@core/utils/formatters'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { storageAttributes } from '@/api/constants'
// i18n // i18n
const { t } = useI18n() const { t } = useI18n()

View File

@@ -0,0 +1,470 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { TorrentCacheData, TorrentCacheItem } from '@/api/types'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
import { formatFileSize, formatDateDifference } from '@core/utils/formatters'
import { useConfirm } from '@/composables/useConfirm'
// 国际化
const { t } = useI18n()
const display = useDisplay()
const appMode = inject('pwaMode') && display.mdAndDown.value
// 从 provide 中获取全局设置
const globalSettings: any = inject('globalSettings')
// 确认框
const createConfirm = useConfirm()
// 提示框
const $toast = useToast()
// 缓存数据
const cacheData = ref<TorrentCacheData>({
count: 0,
sites: 0,
data: [],
})
// 筛选条件
const titleFilter = ref<string | null>(null)
const siteFilter = ref<string | null>(null)
// 获取所有站点选项
const siteOptions = computed(() => {
const sites = new Set<string>()
cacheData.value.data.forEach(item => {
if (item.site_name) {
sites.add(item.site_name)
}
})
return Array.from(sites).sort()
})
// 筛选后的数据
const filteredData = computed(() => {
return cacheData.value.data.filter(item => {
const titleMatch = !titleFilter.value || item.title?.toLowerCase().includes(titleFilter.value?.toLowerCase())
const siteMatch = !siteFilter.value || item.site_name === siteFilter.value
return titleMatch && siteMatch
})
})
// 选中的缓存项
const selectedItems = ref<string[]>([])
// 加载状态
const loading = ref(false)
// 重新识别对话框
const reidentifyDialog = ref(false)
const currentReidentifyItem = ref<TorrentCacheItem | null>(null)
const tmdbId = ref<number | undefined>()
const doubanId = ref<string | undefined>()
const tableStyle = computed(() => {
return appMode ? '' : 'height: calc(100vh - 21rem - env(safe-area-inset-bottom)'
})
// 调用API加载缓存数据
async function loadCacheData() {
try {
loading.value = true
const res: any = await api.get('torrent/cache')
cacheData.value = res.data
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.loadFailed'))
} finally {
loading.value = false
}
}
// 清空所有缓存
async function clearAllCache() {
const isConfirmed = await createConfirm({
type: 'warn',
title: t('common.confirm'),
content: t('setting.cache.clearConfirm'),
})
if (!isConfirmed) return
try {
loading.value = true
await api.delete('torrent/cache')
$toast.success(t('setting.cache.clearSuccess'))
await loadCacheData()
selectedItems.value = []
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.clearFailed'))
} finally {
loading.value = false
}
}
// 刷新缓存
async function refreshCache() {
try {
loading.value = true
const res: any = await api.post('torrent/cache/refresh')
$toast.success(res.message || t('setting.cache.refreshSuccess'))
await loadCacheData()
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.refreshFailed'))
} finally {
loading.value = false
}
}
// 删除选中的缓存项
async function deleteSelectedItems() {
if (selectedItems.value.length === 0) {
$toast.warning(t('setting.cache.selectDeleteWarning'))
return
}
try {
loading.value = true
const deletePromises = selectedItems.value.map(hash => {
const item = cacheData.value.data.find(d => d.hash === hash)
if (item) {
return api.delete(`torrent/cache/${item.domain}/${hash}`)
}
return Promise.resolve()
})
await Promise.all(deletePromises)
$toast.success(t('setting.cache.deleteSelectedSuccess', { count: selectedItems.value.length }))
await loadCacheData()
selectedItems.value = []
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.deleteSelectedFailed'))
} finally {
loading.value = false
}
}
// 删除单个缓存项
async function deleteSingleItem(item: TorrentCacheItem) {
try {
loading.value = true
await api.delete(`torrent/cache/${item.domain}/${item.hash}`)
$toast.success(t('setting.cache.deleteSuccess'))
await loadCacheData()
// 从选中列表中移除
const index = selectedItems.value.indexOf(item.hash)
if (index > -1) {
selectedItems.value.splice(index, 1)
}
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.deleteFailed'))
} finally {
loading.value = false
}
}
// 打开重新识别对话框
function openReidentifyDialog(item: TorrentCacheItem) {
currentReidentifyItem.value = item
tmdbId.value = undefined
doubanId.value = undefined
reidentifyDialog.value = true
}
// 重新识别
async function performReidentify() {
if (!currentReidentifyItem.value) return
try {
loading.value = true
const params: any = {}
if (tmdbId.value) params.tmdbid = tmdbId.value
if (doubanId.value) params.doubanid = doubanId.value
const res: any = await api.post(
`torrent/cache/reidentify/${currentReidentifyItem.value.domain}/${currentReidentifyItem.value.hash}`,
null,
{
params,
},
)
$toast.success(res.message || t('setting.cache.reidentifySuccess'))
await loadCacheData()
reidentifyDialog.value = false
} catch (e) {
console.log(e)
$toast.error(t('setting.cache.reidentifyFailed'))
} finally {
loading.value = false
}
}
// 获取媒体类型颜色
function getMediaTypeColor(type: string): string {
switch (type) {
case t('setting.cache.mediaType.movie'):
return 'primary'
case t('setting.cache.mediaType.tv'):
return 'success'
default:
return 'default'
}
}
// 打开详情页面
function openPageUrl(url: string) {
window.open(url, '_blank')
}
onMounted(() => {
loadCacheData()
})
</script>
<template>
<VCard>
<VCardItem>
<VCardTitle>{{ t('setting.cache.title') }}</VCardTitle>
<VCardSubtitle>{{ t('setting.cache.subtitle') }}</VCardSubtitle>
<template #append>
<div class="d-flex gap-2">
<VBtn icon color="primary" :loading="loading" @click="refreshCache">
<VIcon>mdi-refresh</VIcon>
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.refresh') }}</VTooltip>
</VBtn>
<VBtn
icon
color="warning"
:loading="loading"
:disabled="selectedItems.length === 0"
@click="deleteSelectedItems"
>
<VIcon>mdi-delete-sweep</VIcon>
<VTooltip activator="parent" location="bottom"
>{{ t('setting.cache.deleteSelected') }} ({{ selectedItems.length }})</VTooltip
>
</VBtn>
<VBtn icon color="error" :loading="loading" @click="clearAllCache">
<VIcon>mdi-delete-variant</VIcon>
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.clearAll') }}</VTooltip>
</VBtn>
</div>
</template>
</VCardItem>
<!-- 筛选框 -->
<VCardText>
<VRow>
<VCol cols="6">
<VTextField
v-model="titleFilter"
:label="t('setting.cache.filterByTitle')"
prepend-inner-icon="mdi-magnify"
clearable
density="compact"
/>
</VCol>
<VCol cols="6">
<VSelect
v-model="siteFilter"
:label="t('setting.cache.filterBySite')"
:items="siteOptions"
prepend-inner-icon="mdi-web"
clearable
density="compact"
:placeholder="t('setting.cache.selectSite')"
/>
</VCol>
</VRow>
</VCardText>
<!-- 缓存列表 -->
<VDataTable
v-model="selectedItems"
:headers="[
{ title: '', key: 'data-table-select', sortable: false, width: '48px' },
{ title: t('setting.cache.poster'), key: 'poster', sortable: false, width: '80px' },
{ title: t('setting.cache.torrentTitle'), key: 'title', sortable: true },
{ title: t('setting.cache.site'), key: 'site_name', sortable: true, width: '120px' },
{ title: t('setting.cache.size'), key: 'size', sortable: true, width: '100px' },
{ title: t('setting.cache.publishTime'), key: 'pubdate', sortable: true, width: '150px' },
{ title: t('setting.cache.recognitionResult'), key: 'media_info', sortable: false, width: '200px' },
{ title: t('setting.cache.actions'), key: 'actions', sortable: false, width: '150px' },
]"
:items="filteredData"
:loading="loading"
item-value="hash"
show-select
hover
fixed-header
:items-per-page-text="t('common.itemsPerPage')"
:no-data-text="t('common.noDataText')"
:loading-text="t('common.loadingText')"
:style="tableStyle"
>
<!-- 全选复选框 -->
<template #header.data-table-select="{ allSelected, selectAll, someSelected }">
<VCheckbox
:indeterminate="someSelected && !allSelected"
:model-value="allSelected"
@update:model-value="(value: boolean | null) => selectAll(value as boolean)"
/>
</template>
<!-- 海报列 -->
<template #item.poster="{ item }">
<div class="text-center">
<VImg
v-if="item.poster_path"
:src="item.poster_path"
:alt="item.media_name || item.title"
cover
rounded="md"
class="w-12 my-1 ms-auto"
/>
<VIcon v-else size="x-large" color="grey-lighten-1">
{{ item.media_type === 'movie' ? 'mdi-movie-open' : 'mdi-television-play' }}
</VIcon>
</div>
</template>
<!-- 标题列 -->
<template #item.title="{ item }">
<div class="d-flex flex-column min-w-40">
<div class="text-subtitle-2 font-weight-bold">
{{ item.title }}
</div>
<div v-if="item.description" class="text-caption text-grey">
{{ item.description }}
</div>
<div v-if="item.season_episode || item.resource_term" class="text-caption text-primary mt-1">
{{ item.season_episode }} {{ item.resource_term }}
</div>
</div>
</template>
<!-- 大小列 -->
<template #item.size="{ item }">
{{ formatFileSize(item.size) }}
</template>
<!-- 发布时间列 -->
<template #item.pubdate="{ item }">
{{ formatDateDifference(item.pubdate || '') }}
</template>
<!-- 识别结果列 -->
<template #item.media_info="{ item }">
<div v-if="item.media_name" class="d-flex flex-column">
<div class="text-subtitle-2">
{{ item.media_name }}
<span v-if="item.media_year" class="text-caption text-grey"> ({{ item.media_year }}) </span>
</div>
<div>
<VChip v-if="item.media_type" :color="getMediaTypeColor(item.media_type)" size="x-small">
{{ item.media_type }}
</VChip>
</div>
</div>
<div v-else class="text-caption text-grey">
{{ t('setting.cache.unrecognized') }}
</div>
</template>
<!-- 操作列 -->
<template #item.actions="{ item }">
<div class="d-flex gap-1">
<VBtn icon size="small" color="primary" variant="text" @click="openReidentifyDialog(item)">
<VIcon size="16">mdi-text-recognition</VIcon>
</VBtn>
<VBtn icon size="small" color="error" variant="text" @click="deleteSingleItem(item)">
<VIcon size="16">mdi-delete</VIcon>
</VBtn>
<VBtn
v-if="item.page_url"
icon
size="small"
color="info"
variant="text"
@click="openPageUrl(item.page_url || '')"
target="_blank"
>
<VIcon size="16">mdi-open-in-new</VIcon>
</VBtn>
</div>
</template>
<!-- 空状态 -->
<template #no-data>
<div class="text-center pa-4">
<VIcon size="64" class="mb-4"> mdi-database-off </VIcon>
<div class="text-body-2 text-grey">
{{ t('setting.cache.noData') }}
</div>
</div>
</template>
</VDataTable>
</VCard>
<!-- 重新识别对话框 -->
<VDialog v-model="reidentifyDialog" scrollable max-width="35rem">
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon>mdi-text-recognition</VIcon>
</template>
<VCardTitle>{{ t('setting.cache.reidentifyDialog.title') }}</VCardTitle>
<VCardSubtitle>{{ currentReidentifyItem?.title }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="reidentifyDialog = false" />
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VTextField
v-if="globalSettings.RECOGNIZE_SOURCE === 'themoviedb'"
v-model="tmdbId"
:label="t('setting.cache.reidentifyDialog.tmdbId')"
:hint="t('setting.cache.reidentifyDialog.tmdbIdHint')"
clearable
prepend-inner-icon="mdi-id-card"
persistent-hint
/>
<VTextField
v-else
v-model="doubanId"
:label="t('setting.cache.reidentifyDialog.doubanId')"
:hint="t('setting.cache.reidentifyDialog.doubanIdHint')"
clearable
prepend-inner-icon="mdi-id-card"
persistent-hint
/>
</VCol>
</VRow>
<VAlert type="info" variant="tonal" class="mt-4">
{{ t('setting.cache.reidentifyDialog.autoHint') }}
</VAlert>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn color="primary" :loading="loading" prepend-icon="mdi-check" @click="performReidentify">
{{ t('setting.cache.reidentifyDialog.confirm') }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>

View File

@@ -239,7 +239,9 @@ onMounted(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" class="me-2" @click="saveStorages"> {{ t('common.save') }} </VBtn> <VBtn type="submit" class="me-2" @click="saveStorages" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtn color="success" variant="tonal" @click="addStorage"> <VBtn color="success" variant="tonal" @click="addStorage">
<VIcon icon="mdi-plus" /> <VIcon icon="mdi-plus" />
</VBtn> </VBtn>
@@ -279,7 +281,9 @@ onMounted(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveDirectories"> {{ t('common.save') }} </VBtn> <VBtn type="submit" @click="saveDirectories" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtn color="success" variant="tonal" @click="addDirectory"> <VBtn color="success" variant="tonal" @click="addDirectory">
<VIcon icon="mdi-plus" /> <VIcon icon="mdi-plus" />
</VBtn> </VBtn>
@@ -305,6 +309,7 @@ onMounted(() => {
:label="t('setting.directory.scrapSource')" :label="t('setting.directory.scrapSource')"
:hint="t('setting.directory.scrapSourceHint')" :hint="t('setting.directory.scrapSourceHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-database"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
@@ -315,6 +320,7 @@ onMounted(() => {
persistent-hint persistent-hint
clearable clearable
active active
prepend-inner-icon="mdi-movie-open"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
@@ -325,6 +331,7 @@ onMounted(() => {
persistent-hint persistent-hint
clearable clearable
active active
prepend-inner-icon="mdi-television"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -332,7 +339,9 @@ onMounted(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSystemSettings(SystemSettings.Basic)"> {{ t('common.save') }}</VBtn> <VBtn type="submit" @click="saveSystemSettings(SystemSettings.Basic)" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div> </div>
</VForm> </VForm>
</VCardText> </VCardText>

View File

@@ -293,7 +293,9 @@ onMounted(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn mtype="submit" @click="saveNotificationSetting"> {{ t('common.save') }} </VBtn> <VBtn mtype="submit" @click="saveNotificationSetting" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtn color="success" variant="tonal"> <VBtn color="success" variant="tonal">
<VIcon icon="mdi-plus" /> <VIcon icon="mdi-plus" />
<VMenu :activator="'parent'" :close-on-content-click="true"> <VMenu :activator="'parent'" :close-on-content-click="true">
@@ -401,7 +403,9 @@ onMounted(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveNotificationSwitchs"> {{ t('common.save') }} </VBtn> <VBtn type="submit" @click="saveNotificationSwitchs" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div> </div>
</VForm> </VForm>
</VCardText> </VCardText>
@@ -418,17 +422,29 @@ onMounted(() => {
<VCardText> <VCardText>
<VRow> <VRow>
<VCol cols="6"> <VCol cols="6">
<VTextField v-model="notificationTime.start" :label="t('setting.notification.startTime')" type="time" /> <VTextField
v-model="notificationTime.start"
:label="t('setting.notification.startTime')"
type="time"
prepend-inner-icon="mdi-clock-start"
/>
</VCol> </VCol>
<VCol cols="6"> <VCol cols="6">
<VTextField v-model="notificationTime.end" :label="t('setting.notification.endTime')" type="time" /> <VTextField
v-model="notificationTime.end"
:label="t('setting.notification.endTime')"
type="time"
prepend-inner-icon="mdi-clock-end"
/>
</VCol> </VCol>
</VRow> </VRow>
</VCardText> </VCardText>
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveNotificationTime"> {{ t('common.save') }} </VBtn> <VBtn type="submit" @click="saveNotificationTime" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div> </div>
</VForm> </VForm>
</VCardText> </VCardText>
@@ -445,11 +461,16 @@ onMounted(() => {
<!-- 模板编辑器对话框 --> <!-- 模板编辑器对话框 -->
<VDialog v-model="editorVisible" v-if="editorVisible" max-width="50rem" :fullscreen="!display.mdAndUp.value"> <VDialog v-model="editorVisible" v-if="editorVisible" max-width="50rem" :fullscreen="!display.mdAndUp.value">
<VCard> <VCard>
<VCardItem> <VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-code-json" class="me-2" />
</template>
<VCardTitle> <VCardTitle>
{{ templateTypes.find(t => t.type === currentTemplate)?.label }}
{{ t('setting.notification.templateConfigTitle') }} {{ t('setting.notification.templateConfigTitle') }}
</VCardTitle> </VCardTitle>
<VCardSubtitle>
{{ templateTypes.find(t => t.type === currentTemplate)?.label }}
</VCardSubtitle>
<VDialogCloseBtn @click="editorVisible = false" /> <VDialogCloseBtn @click="editorVisible = false" />
</VCardItem> </VCardItem>
<VCardText class="py-0"> <VCardText class="py-0">

View File

@@ -401,7 +401,9 @@ onMounted(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" class="me-2" @click="saveCustomRules"> {{ t('common.save') }} </VBtn> <VBtn type="submit" class="me-2" @click="saveCustomRules" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtnGroup density="comfortable"> <VBtnGroup density="comfortable">
<VBtn color="success" variant="tonal" @click="addCustomRule"> <VBtn color="success" variant="tonal" @click="addCustomRule">
<VIcon icon="mdi-plus" /> <VIcon icon="mdi-plus" />
@@ -452,7 +454,9 @@ onMounted(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" class="me-2" @click="saveFilterRuleGroups"> {{ t('common.save') }} </VBtn> <VBtn type="submit" class="me-2" @click="saveFilterRuleGroups" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtnGroup density="comfortable"> <VBtnGroup density="comfortable">
<VBtn color="success" variant="tonal" @click="addFilterRuleGroup"> <VBtn color="success" variant="tonal" @click="addFilterRuleGroup">
<VIcon icon="mdi-plus" /> <VIcon icon="mdi-plus" />
@@ -501,6 +505,7 @@ onMounted(() => {
:label="t('setting.rule.currentPriorityRules')" :label="t('setting.rule.currentPriorityRules')"
:hint="t('setting.rule.currentPriorityRulesHint')" :hint="t('setting.rule.currentPriorityRulesHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-priority-high"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -509,7 +514,9 @@ onMounted(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveTorrentPriority"> {{ t('common.save') }} </VBtn> <VBtn type="submit" @click="saveTorrentPriority" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div> </div>
</VForm> </VForm>
</VCardText> </VCardText>

View File

@@ -205,6 +205,7 @@ onMounted(() => {
:label="t('setting.search.mediaSource')" :label="t('setting.search.mediaSource')"
:hint="t('setting.search.mediaSourceHint')" :hint="t('setting.search.mediaSourceHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-database-search"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -217,6 +218,7 @@ onMounted(() => {
:label="t('setting.search.filterRuleGroup')" :label="t('setting.search.filterRuleGroup')"
:hint="t('setting.search.filterRuleGroupHint')" :hint="t('setting.search.filterRuleGroupHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-filter"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -228,6 +230,7 @@ onMounted(() => {
placeholder="MOVIEPILOT" placeholder="MOVIEPILOT"
:hint="t('setting.search.downloadLabelHint')" :hint="t('setting.search.downloadLabelHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-tag"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -237,6 +240,7 @@ onMounted(() => {
:placeholder="t('setting.search.downloadUserPlaceholder')" :placeholder="t('setting.search.downloadUserPlaceholder')"
:hint="t('setting.search.downloadUserHint')" :hint="t('setting.search.downloadUserHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-account"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -260,7 +264,9 @@ onMounted(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSearchSetting"> {{ t('common.save') }} </VBtn> <VBtn type="submit" @click="saveSearchSetting" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div> </div>
</VForm> </VForm>
</VCardText> </VCardText>
@@ -291,7 +297,9 @@ onMounted(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSelectedSites"> {{ t('common.save') }} </VBtn> <VBtn type="submit" @click="saveSelectedSites" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div> </div>
</VForm> </VForm>
</VCardText> </VCardText>

View File

@@ -161,6 +161,7 @@ onMounted(() => {
:disabled="siteSetting.CookieCloud.COOKIECLOUD_ENABLE_LOCAL" :disabled="siteSetting.CookieCloud.COOKIECLOUD_ENABLE_LOCAL"
:hint="t('setting.site.serviceAddressHint')" :hint="t('setting.site.serviceAddressHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-server"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -169,6 +170,7 @@ onMounted(() => {
:label="t('setting.site.userKey')" :label="t('setting.site.userKey')"
:hint="t('setting.site.userKeyHint')" :hint="t('setting.site.userKeyHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-key"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -180,6 +182,7 @@ onMounted(() => {
:label="t('setting.site.e2ePassword')" :label="t('setting.site.e2ePassword')"
:hint="t('setting.site.e2ePasswordHint')" :hint="t('setting.site.e2ePasswordHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-lock"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -189,6 +192,7 @@ onMounted(() => {
:items="CookieCloudIntervalItems" :items="CookieCloudIntervalItems"
:hint="t('setting.site.autoSyncIntervalHint')" :hint="t('setting.site.autoSyncIntervalHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-timer"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -198,6 +202,7 @@ onMounted(() => {
:placeholder="t('setting.site.syncBlacklistPlaceholder')" :placeholder="t('setting.site.syncBlacklistPlaceholder')"
:hint="t('setting.site.syncBlacklistHint')" :hint="t('setting.site.syncBlacklistHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-block-helper"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -206,6 +211,7 @@ onMounted(() => {
:label="t('setting.site.userAgent')" :label="t('setting.site.userAgent')"
:hint="t('setting.site.userAgentHint')" :hint="t('setting.site.userAgentHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-web"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -214,7 +220,9 @@ onMounted(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSiteSetting(siteSetting.CookieCloud)"> {{ t('common.save') }} </VBtn> <VBtn type="submit" @click="saveSiteSetting(siteSetting.CookieCloud)" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div> </div>
</VForm> </VForm>
</VCardText> </VCardText>
@@ -234,6 +242,7 @@ onMounted(() => {
:items="SiteDataRefreshIntervalItems" :items="SiteDataRefreshIntervalItems"
:hint="t('setting.site.siteDataRefreshIntervalHint')" :hint="t('setting.site.siteDataRefreshIntervalHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-refresh"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -252,7 +261,9 @@ onMounted(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSiteSetting(siteSetting.Site)"> {{ t('common.save') }} </VBtn> <VBtn type="submit" @click="saveSiteSetting(siteSetting.Site)" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div> </div>
</VForm> </VForm>
</VCardText> </VCardText>

View File

@@ -217,6 +217,7 @@ onMounted(() => {
:label="t('setting.subscribe.mode')" :label="t('setting.subscribe.mode')"
:hint="t('setting.subscribe.modeHint')" :hint="t('setting.subscribe.modeHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-cog"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -226,6 +227,7 @@ onMounted(() => {
:label="t('setting.subscribe.rssInterval')" :label="t('setting.subscribe.rssInterval')"
:hint="t('setting.subscribe.rssIntervalHint')" :hint="t('setting.subscribe.rssIntervalHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-timer"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -238,6 +240,7 @@ onMounted(() => {
:label="t('setting.subscribe.filterRuleGroup')" :label="t('setting.subscribe.filterRuleGroup')"
:hint="t('setting.subscribe.filterRuleGroupHint')" :hint="t('setting.subscribe.filterRuleGroupHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-filter"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -250,6 +253,7 @@ onMounted(() => {
:label="t('setting.subscribe.bestVersionRuleGroup')" :label="t('setting.subscribe.bestVersionRuleGroup')"
:hint="t('setting.subscribe.bestVersionRuleGroupHint')" :hint="t('setting.subscribe.bestVersionRuleGroupHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-star"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -276,7 +280,9 @@ onMounted(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSubscribeSetting"> {{ t('common.save') }} </VBtn> <VBtn type="submit" @click="saveSubscribeSetting" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div> </div>
</VForm> </VForm>
</VCardText> </VCardText>
@@ -307,7 +313,9 @@ onMounted(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveSelectedRssSites"> {{ t('common.save') }} </VBtn> <VBtn type="submit" @click="saveSelectedRssSites" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div> </div>
</VForm> </VForm>
</VCardText> </VCardText>

View File

@@ -40,6 +40,8 @@ const SystemSettings = ref<any>({
PLUGIN_STATISTIC_SHARE: true, PLUGIN_STATISTIC_SHARE: true,
BIG_MEMORY_MODE: false, BIG_MEMORY_MODE: false,
DB_WAL_ENABLE: false, DB_WAL_ENABLE: false,
AUTO_UPDATE_RESOURCE: true,
MOVIEPILOT_AUTO_UPDATE: false,
// 媒体 // 媒体
TMDB_API_DOMAIN: null, TMDB_API_DOMAIN: null,
TMDB_IMAGE_DOMAIN: null, TMDB_IMAGE_DOMAIN: null,
@@ -385,6 +387,16 @@ function onMediaServerChange(mediaserver: MediaServerConf, name: string) {
if (index !== -1) mediaServers.value[index] = mediaserver if (index !== -1) mediaServers.value[index] = mediaserver
} }
// 添加计算属性
const moviePilotAutoUpdate = computed({
get: () => {
return ['release', 'dev'].includes(SystemSettings.value.Advanced.MOVIEPILOT_AUTO_UPDATE)
},
set: val => {
SystemSettings.value.Advanced.MOVIEPILOT_AUTO_UPDATE = val ? 'release' : 'false'
},
})
// 加载数据 // 加载数据
onMounted(() => { onMounted(() => {
loadDownloaderSetting() loadDownloaderSetting()
@@ -426,6 +438,7 @@ onDeactivated(() => {
:hint="t('setting.system.appDomainHint')" :hint="t('setting.system.appDomainHint')"
placeholder="http://localhost:3000" placeholder="http://localhost:3000"
persistent-hint persistent-hint
prepend-inner-icon="mdi-web"
/> />
</VCol> </VCol>
@@ -438,6 +451,7 @@ onDeactivated(() => {
:hint="t('setting.system.wallpaperHint')" :hint="t('setting.system.wallpaperHint')"
persistent-hint persistent-hint
:items="wallpaperItems" :items="wallpaperItems"
prepend-inner-icon="mdi-image"
/> />
</VCol> </VCol>
@@ -449,6 +463,7 @@ onDeactivated(() => {
:placeholder="t('setting.system.customizeWallpaperApi')" :placeholder="t('setting.system.customizeWallpaperApi')"
persistent-hint persistent-hint
:rules="[v => !!v || t('setting.system.customizeWallpaperApiRequired')]" :rules="[v => !!v || t('setting.system.customizeWallpaperApiRequired')]"
prepend-inner-icon="mdi-api"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -463,6 +478,7 @@ onDeactivated(() => {
{ title: 'TheMovieDb', value: 'themoviedb' }, { title: 'TheMovieDb', value: 'themoviedb' },
{ title: '豆瓣', value: 'douban' }, { title: '豆瓣', value: 'douban' },
]" ]"
prepend-inner-icon="mdi-database"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -479,6 +495,7 @@ onDeactivated(() => {
(v: any) => !isNaN(v) || t('setting.system.numbersOnly'), (v: any) => !isNaN(v) || t('setting.system.numbersOnly'),
(v: any) => v >= 1 || t('setting.system.minInterval'), (v: any) => v >= 1 || t('setting.system.minInterval'),
]" ]"
prepend-inner-icon="mdi-sync"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -488,10 +505,11 @@ onDeactivated(() => {
:hint="t('setting.system.apiTokenHint')" :hint="t('setting.system.apiTokenHint')"
:placeholder="t('setting.system.apiTokenMinChars')" :placeholder="t('setting.system.apiTokenMinChars')"
persistent-hint persistent-hint
prependInnerIcon="mdi-reload" prepend-inner-icon="mdi-key"
:appendInnerIcon="SystemSettings.Basic.API_TOKEN ? 'mdi-content-copy' : ''" :append-inner-icon="SystemSettings.Basic.API_TOKEN ? 'mdi-content-copy' : 'mdi-reload'"
@click:prependInner="createRandomString" @click:append-inner="
@click:appendInner="copyValue(SystemSettings.Basic.API_TOKEN)" SystemSettings.Basic.API_TOKEN ? copyValue(SystemSettings.Basic.API_TOKEN) : createRandomString()
"
:rules="[ :rules="[
(v: string) => !!v || t('setting.system.apiTokenRequired'), (v: string) => !!v || t('setting.system.apiTokenRequired'),
(v: string) => v.length >= 16 || t('setting.system.apiTokenLength'), (v: string) => v.length >= 16 || t('setting.system.apiTokenLength'),
@@ -505,6 +523,7 @@ onDeactivated(() => {
:placeholder="t('setting.system.githubTokenFormat')" :placeholder="t('setting.system.githubTokenFormat')"
:hint="t('setting.system.githubTokenHint')" :hint="t('setting.system.githubTokenHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-github"
> >
</VTextField> </VTextField>
</VCol> </VCol>
@@ -515,6 +534,7 @@ onDeactivated(() => {
placeholder="https://movie-pilot.org" placeholder="https://movie-pilot.org"
:hint="t('setting.system.ocrHostHint')" :hint="t('setting.system.ocrHostHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-text-recognition"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -523,7 +543,9 @@ onDeactivated(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveBasicSettings"> {{ t('common.save') }} </VBtn> <VBtn type="submit" @click="saveBasicSettings" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VSpacer /> <VSpacer />
<VBtn <VBtn
color="error" color="error"
@@ -568,7 +590,9 @@ onDeactivated(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveDownloaderSetting"> {{ t('common.save') }} </VBtn> <VBtn type="submit" @click="saveDownloaderSetting" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtn color="success" variant="tonal"> <VBtn color="success" variant="tonal">
<VIcon icon="mdi-plus" /> <VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click> <VMenu activator="parent" close-on-content-click>
@@ -616,7 +640,9 @@ onDeactivated(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveMediaServerSetting"> {{ t('common.save') }} </VBtn> <VBtn type="submit" @click="saveMediaServerSetting" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtn color="success" variant="tonal"> <VBtn color="success" variant="tonal">
<VIcon icon="mdi-plus" /> <VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click> <VMenu activator="parent" close-on-content-click>
@@ -721,6 +747,22 @@ onDeactivated(() => {
persistent-hint persistent-hint
/> />
</VCol> </VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="moviePilotAutoUpdate"
:label="t('setting.system.moviePilotAutoUpdate')"
:hint="t('setting.system.moviePilotAutoUpdateHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.AUTO_UPDATE_RESOURCE"
:label="t('setting.system.autoUpdateResource')"
:hint="t('setting.system.autoUpdateResourceHint')"
persistent-hint
/>
</VCol>
</VRow> </VRow>
</div> </div>
</VWindowItem> </VWindowItem>
@@ -736,6 +778,7 @@ onDeactivated(() => {
persistent-hint persistent-hint
:items="['api.themoviedb.org', 'api.tmdb.org']" :items="['api.themoviedb.org', 'api.tmdb.org']"
:rules="[(v: string) => !!v || t('setting.system.tmdbApiDomainRequired')]" :rules="[(v: string) => !!v || t('setting.system.tmdbApiDomainRequired')]"
prepend-inner-icon="mdi-api"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -747,6 +790,7 @@ onDeactivated(() => {
persistent-hint persistent-hint
:items="['image.tmdb.org', 'static-mdb.v.geilijiasu.com']" :items="['image.tmdb.org', 'static-mdb.v.geilijiasu.com']"
:rules="[(v: string) => !!v || t('setting.system.tmdbImageDomainRequired')]" :rules="[(v: string) => !!v || t('setting.system.tmdbImageDomainRequired')]"
prepend-inner-icon="mdi-image"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -757,6 +801,7 @@ onDeactivated(() => {
:hint="t('setting.system.tmdbLocaleHint')" :hint="t('setting.system.tmdbLocaleHint')"
persistent-hint persistent-hint
:items="tmdbLanguageItems" :items="tmdbLanguageItems"
prepend-inner-icon="mdi-translate"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -772,6 +817,7 @@ onDeactivated(() => {
(v: any) => v === 0 || !!v || t('setting.system.metaCacheExpireRequired'), (v: any) => v === 0 || !!v || t('setting.system.metaCacheExpireRequired'),
(v: any) => v >= 0 || t('setting.system.metaCacheExpireMin'), (v: any) => v >= 0 || t('setting.system.metaCacheExpireMin'),
]" ]"
prepend-inner-icon="mdi-timer"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -806,6 +852,16 @@ onDeactivated(() => {
<VWindowItem value="network"> <VWindowItem value="network">
<div> <div>
<VRow> <VRow>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Advanced.PROXY_HOST"
:label="t('setting.system.proxyHost')"
placeholder="http://127.0.0.1:7890"
:hint="t('setting.system.proxyHostHint')"
persistent-hint
prepend-inner-icon="mdi-server-network"
/>
</VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VCombobox <VCombobox
v-model="githubProxyDisplay" v-model="githubProxyDisplay"
@@ -815,9 +871,10 @@ onDeactivated(() => {
persistent-hint persistent-hint
:items="githubMirrorsItems" :items="githubMirrorsItems"
clearable clearable
prepend-inner-icon="mdi-github"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12">
<VCombobox <VCombobox
v-model="pipProxyDisplay" v-model="pipProxyDisplay"
:label="t('setting.system.pipProxy')" :label="t('setting.system.pipProxy')"
@@ -826,6 +883,7 @@ onDeactivated(() => {
persistent-hint persistent-hint
:items="pipMirrorsItems" :items="pipMirrorsItems"
clearable clearable
prepend-inner-icon="mdi-package"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -845,6 +903,7 @@ onDeactivated(() => {
:placeholder="t('setting.system.dohResolversPlaceholder')" :placeholder="t('setting.system.dohResolversPlaceholder')"
:hint="t('setting.system.dohResolversHint')" :hint="t('setting.system.dohResolversHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-dns"
/> />
</VCol> </VCol>
<VCol cols="12" v-show="SystemSettings.Advanced.DOH_ENABLE"> <VCol cols="12" v-show="SystemSettings.Advanced.DOH_ENABLE">
@@ -854,6 +913,7 @@ onDeactivated(() => {
:placeholder="t('setting.system.dohDomainsPlaceholder')" :placeholder="t('setting.system.dohDomainsPlaceholder')"
:hint="t('setting.system.dohDomainsHint')" :hint="t('setting.system.dohDomainsHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-domain"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -885,6 +945,7 @@ onDeactivated(() => {
:placeholder="t('setting.system.securityImageDomainAdd')" :placeholder="t('setting.system.securityImageDomainAdd')"
hide-details hide-details
density="compact" density="compact"
prepend-inner-icon="mdi-shield-check"
> >
<template #append> <template #append>
<VBtn icon color="primary" @click="addSecurityDomain" :disabled="!newSecurityDomain"> <VBtn icon color="primary" @click="addSecurityDomain" :disabled="!newSecurityDomain">
@@ -918,6 +979,7 @@ onDeactivated(() => {
:hint="t('setting.system.logLevelHint')" :hint="t('setting.system.logLevelHint')"
persistent-hint persistent-hint
:items="logLevelItems" :items="logLevelItems"
prepend-inner-icon="mdi-format-list-bulleted"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -930,6 +992,7 @@ onDeactivated(() => {
type="number" type="number"
:suffix="t('setting.system.mb')" :suffix="t('setting.system.mb')"
:rules="[(v: any) => v === 0 || !!v || t('setting.system.logMaxFileSizeRequired'), (v: any) => v >= 1 || t('setting.system.logMaxFileSizeMin')]" :rules="[(v: any) => v === 0 || !!v || t('setting.system.logMaxFileSizeRequired'), (v: any) => v >= 1 || t('setting.system.logMaxFileSizeMin')]"
prepend-inner-icon="mdi-file-document"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -941,6 +1004,7 @@ onDeactivated(() => {
min="1" min="1"
type="number" type="number"
:rules="[(v: any) => v === 0 || !!v || t('setting.system.logBackupCountRequired'), (v: any) => v >= 1 || t('setting.system.logBackupCountMin')]" :rules="[(v: any) => v === 0 || !!v || t('setting.system.logBackupCountRequired'), (v: any) => v >= 1 || t('setting.system.logBackupCountMin')]"
prepend-inner-icon="mdi-backup-restore"
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
@@ -949,6 +1013,7 @@ onDeactivated(() => {
:label="t('setting.system.logFileFormat')" :label="t('setting.system.logFileFormat')"
:hint="t('setting.system.logFileFormatHint')" :hint="t('setting.system.logFileFormatHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-format-text"
/> />
</VCol> </VCol>
</VRow> </VRow>

View File

@@ -143,6 +143,7 @@ onMounted(() => {
:placeholder="t('setting.words.identifiersPlaceholder')" :placeholder="t('setting.words.identifiersPlaceholder')"
:hint="t('setting.words.identifiersHint')" :hint="t('setting.words.identifiersHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-tag-text"
/> />
</VCardText> </VCardText>
<VCardText> <VCardText>
@@ -153,7 +154,9 @@ onMounted(() => {
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveCustomIdentifiers">{{ t('common.save') }}</VBtn> <VBtn type="submit" @click="saveCustomIdentifiers" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div> </div>
</VForm> </VForm>
</VCardText> </VCardText>
@@ -173,12 +176,15 @@ onMounted(() => {
:placeholder="t('setting.words.releaseGroupsPlaceholder')" :placeholder="t('setting.words.releaseGroupsPlaceholder')"
:hint="t('setting.words.releaseGroupsHint')" :hint="t('setting.words.releaseGroupsHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-account-group"
/> />
</VCardText> </VCardText>
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveCustomReleaseGroups">{{ t('common.save') }}</VBtn> <VBtn type="submit" @click="saveCustomReleaseGroups" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div> </div>
</VForm> </VForm>
</VCardText> </VCardText>
@@ -198,12 +204,15 @@ onMounted(() => {
:placeholder="t('setting.words.customizationPlaceholder')" :placeholder="t('setting.words.customizationPlaceholder')"
:hint="t('setting.words.customizationHint')" :hint="t('setting.words.customizationHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-code-braces"
/> />
</VCardText> </VCardText>
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveCustomization">{{ t('common.save') }}</VBtn> <VBtn type="submit" @click="saveCustomization" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div> </div>
</VForm> </VForm>
</VCardText> </VCardText>
@@ -223,12 +232,15 @@ onMounted(() => {
:placeholder="t('setting.words.excludeWordsPlaceholder')" :placeholder="t('setting.words.excludeWordsPlaceholder')"
:hint="t('setting.words.excludeWordsHint')" :hint="t('setting.words.excludeWordsHint')"
persistent-hint persistent-hint
prepend-inner-icon="mdi-block-helper"
/> />
</VCardText> </VCardText>
<VCardText> <VCardText>
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4"> <div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveTransferExcludeWords">{{ t('common.save') }}</VBtn> <VBtn type="submit" @click="saveTransferExcludeWords" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
</div> </div>
</VForm> </VForm>
</VCardText> </VCardText>

View File

@@ -1,6 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
// 定义输入变量
const props = defineProps<{
logfile: string
}>()
// 国际化 // 国际化
const { t } = useI18n() const { t } = useI18n()
@@ -33,7 +38,12 @@ function getLogColor(level: string): string {
// SSE持续获取日志 // SSE持续获取日志
function startSSELogging() { function startSSELogging() {
eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/logging`) console.log(props.logfile)
eventSource = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/logging?logfile=${
encodeURIComponent(props.logfile) ?? 'moviepilot.log'
}`,
)
const buffer: string[] = [] const buffer: string[] = []
let timeoutId: number | null = null let timeoutId: number | null = null

View File

@@ -54,10 +54,21 @@ async function nameTest() {
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<VRow class="pt-2"> <VRow class="pt-2">
<VCol cols="12"> <VCol cols="12">
<VTextField v-model="nameTestForm.title" :label="t('nameTest.title')" :rules="[requiredValidator]" /> <VTextField
v-model="nameTestForm.title"
:label="t('nameTest.title')"
:rules="[requiredValidator]"
prepend-inner-icon="mdi-movie-open"
/>
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
<VTextarea v-model="nameTestForm.subtitle" :label="t('nameTest.subtitle')" rows="2" auto-grow /> <VTextarea
v-model="nameTestForm.subtitle"
:label="t('nameTest.subtitle')"
rows="2"
auto-grow
prepend-inner-icon="mdi-subtitles"
/>
</VCol> </VCol>
</VRow> </VRow>
<VRow> <VRow>

View File

@@ -80,13 +80,29 @@ onMounted(() => {
<VForm @submit.prevent="() => {}"> <VForm @submit.prevent="() => {}">
<VRow class="pt-2"> <VRow class="pt-2">
<VCol cols="12" md="8"> <VCol cols="12" md="8">
<VTextField v-model="ruleTestForm.title" :label="t('ruleTest.title')" :rules="[requiredValidator]" /> <VTextField
v-model="ruleTestForm.title"
:label="t('ruleTest.title')"
:rules="[requiredValidator]"
prepend-inner-icon="mdi-movie-open"
/>
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<VSelect v-model="ruleTestForm.rulegroup" :label="t('ruleTest.ruleGroup')" :items="filterRuleGroupItems" /> <VSelect
v-model="ruleTestForm.rulegroup"
:label="t('ruleTest.ruleGroup')"
:items="filterRuleGroupItems"
prepend-inner-icon="mdi-filter"
/>
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
<VTextarea v-model="ruleTestForm.subtitle" :label="t('ruleTest.subtitle')" rows="2" auto-grow /> <VTextarea
v-model="ruleTestForm.subtitle"
:label="t('ruleTest.subtitle')"
rows="2"
auto-grow
prepend-inner-icon="mdi-subtitles"
/>
</VCol> </VCol>
</VRow> </VRow>
<VRow> <VRow>

View File

@@ -322,7 +322,13 @@ watch(
<VForm class="mt-6"> <VForm class="mt-6">
<VRow> <VRow>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField v-model="currentUserName" density="comfortable" readonly :label="t('user.username')" /> <VTextField
v-model="currentUserName"
density="comfortable"
readonly
:label="t('user.username')"
prepend-inner-icon="mdi-account"
/>
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
@@ -331,6 +337,7 @@ watch(
clearable clearable
:label="t('user.email')" :label="t('user.email')"
type="email" type="email"
prepend-inner-icon="mdi-email"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -342,6 +349,7 @@ watch(
clearable clearable
:label="t('user.password')" :label="t('user.password')"
autocomplete="" autocomplete=""
prepend-inner-icon="mdi-lock"
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible" @click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
/> />
</VCol> </VCol>
@@ -354,6 +362,7 @@ watch(
:append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'" :append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
clearable clearable
:label="t('user.confirmPassword')" :label="t('user.confirmPassword')"
prepend-inner-icon="mdi-lock-check"
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible" @click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
/> />
</VCol> </VCol>
@@ -364,6 +373,7 @@ watch(
clearable clearable
:label="t('profile.nickname')" :label="t('profile.nickname')"
:placeholder="t('profile.nicknamePlaceholder')" :placeholder="t('profile.nicknamePlaceholder')"
prepend-inner-icon="mdi-card-account-details"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -379,6 +389,7 @@ watch(
density="comfortable" density="comfortable"
clearable clearable
:label="t('profile.wechatUser')" :label="t('profile.wechatUser')"
prepend-inner-icon="mdi-wechat"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -387,6 +398,7 @@ watch(
density="comfortable" density="comfortable"
clearable clearable
:label="t('profile.telegramUser')" :label="t('profile.telegramUser')"
prepend-inner-icon="mdi-send"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -395,6 +407,7 @@ watch(
density="comfortable" density="comfortable"
clearable clearable
:label="t('profile.slackUser')" :label="t('profile.slackUser')"
prepend-inner-icon="mdi-slack"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -403,6 +416,7 @@ watch(
density="comfortable" density="comfortable"
clearable clearable
:label="t('profile.vocechatUser')" :label="t('profile.vocechatUser')"
prepend-inner-icon="mdi-chat"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -411,6 +425,7 @@ watch(
density="comfortable" density="comfortable"
clearable clearable
:label="t('profile.synologychatUser')" :label="t('profile.synologychatUser')"
prepend-inner-icon="mdi-message"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -419,13 +434,14 @@ watch(
density="comfortable" density="comfortable"
clearable clearable
:label="t('profile.doubanUser')" :label="t('profile.doubanUser')"
prepend-inner-icon="mdi-movie"
/> />
</VCol> </VCol>
</VRow> </VRow>
<VRow> <VRow>
<!-- 👉 Form Actions --> <!-- 👉 Form Actions -->
<VCol cols="12" class="d-flex flex-wrap gap-4"> <VCol cols="12" class="d-flex flex-wrap gap-4">
<VBtn @click="saveAccountInfo" :disabled="isSaving"> <VBtn @click="saveAccountInfo" :disabled="isSaving" prepend-icon="mdi-content-save">
<span v-if="isSaving">{{ t('common.saving') }}...</span> <span v-if="isSaving">{{ t('common.saving') }}...</span>
<span v-else>{{ t('common.save') }}</span> <span v-else>{{ t('common.save') }}</span>
</VBtn> </VBtn>
@@ -462,6 +478,7 @@ watch(
autocomplete="" autocomplete=""
class="mb-8" class="mb-8"
variant="outlined" variant="outlined"
prepend-inner-icon="mdi-shield-key"
/> />
<div class="d-flex justify-end flex-wrap gap-4"> <div class="d-flex justify-end flex-wrap gap-4">
<VBtn variant="outlined" color="secondary" @click="otpDialog = false"> {{ t('common.cancel') }} </VBtn> <VBtn variant="outlined" color="secondary" @click="otpDialog = false"> {{ t('common.cancel') }} </VBtn>