mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-11 01:50:10 +08:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c893abcdf | ||
|
|
ead891ca2f | ||
|
|
8713e3cc86 | ||
|
|
3cc83d10d3 | ||
|
|
192ded374a | ||
|
|
13997c7e74 | ||
|
|
71b0dd4cc2 | ||
|
|
a58a0cdffe | ||
|
|
6aeb040db4 | ||
|
|
fef20e361e | ||
|
|
a63a07701d | ||
|
|
5dd56f2db3 | ||
|
|
275b095574 | ||
|
|
05eae71fba | ||
|
|
777b3c9445 | ||
|
|
a214168b1e | ||
|
|
9d55d02557 | ||
|
|
16c084ba80 | ||
|
|
b0f4ccc186 | ||
|
|
96d0606b4d | ||
|
|
450b9ec28a | ||
|
|
2ccf03fc1b | ||
|
|
38dfb3af07 | ||
|
|
ae4c59bfdb | ||
|
|
c9f4fdbee8 | ||
|
|
d21f461dda | ||
|
|
28a5a83315 | ||
|
|
11d11b88bf | ||
|
|
ff7658b5ba | ||
|
|
351faf2891 | ||
|
|
7d66229bad | ||
|
|
2b08be1e7d | ||
|
|
8255cfd479 | ||
|
|
f356bb4407 | ||
|
|
07e60291a2 | ||
|
|
2dbe8e6685 | ||
|
|
40f36b2afd | ||
|
|
d4260d5103 | ||
|
|
45f68bc936 | ||
|
|
9469074837 | ||
|
|
193807bb6f | ||
|
|
d4548db5b9 | ||
|
|
29aaea6fe6 | ||
|
|
369cc6438f | ||
|
|
d80b39c77b | ||
|
|
626725a8ca | ||
|
|
8be96358ae | ||
|
|
f2bfbfa3c5 | ||
|
|
7c9ffd6abc | ||
|
|
b370354287 | ||
|
|
145d71e283 | ||
|
|
eeea82d815 | ||
|
|
babd267bc4 | ||
|
|
e136c931ac | ||
|
|
ae00602345 | ||
|
|
5382108ee7 | ||
|
|
514063d3fb | ||
|
|
b08f396fec | ||
|
|
d37a7f06f1 | ||
|
|
ad7bca3aae | ||
|
|
4fb70ba80e | ||
|
|
1225b2eb9e | ||
|
|
24b2f103b9 | ||
|
|
0d304b58ca | ||
|
|
f419dbd794 | ||
|
|
7854cc81a8 | ||
|
|
9ad1bd29bd | ||
|
|
b88d4f0ecb | ||
|
|
44168b62d2 | ||
|
|
1dab013436 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,6 +13,7 @@ dist
|
||||
dist-ssr
|
||||
dev-dist
|
||||
*.local
|
||||
package-lock.json
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.9.5",
|
||||
"version": "2.9.23",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": "dist/service.js",
|
||||
@@ -127,4 +127,4 @@
|
||||
"workbox-window": "^7.3.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.18"
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,10 @@ export const downloaderOptions = [
|
||||
value: 'transmission',
|
||||
title: i18n.global.t('setting.system.transmission'),
|
||||
},
|
||||
{
|
||||
value: 'rtorrent',
|
||||
title: i18n.global.t('setting.system.rtorrent'),
|
||||
},
|
||||
]
|
||||
|
||||
export const downloaderDict = downloaderOptions.reduce((dict, item) => {
|
||||
@@ -76,6 +80,10 @@ export const mediaServerOptions = [
|
||||
value: 'trimemedia',
|
||||
title: i18n.global.t('setting.system.trimeMedia'),
|
||||
},
|
||||
{
|
||||
value: 'ugreen',
|
||||
title: i18n.global.t('setting.system.ugreen'),
|
||||
},
|
||||
]
|
||||
|
||||
export const mediaServerDict = mediaServerOptions.reduce((dict, item) => {
|
||||
|
||||
@@ -885,8 +885,8 @@ export interface MediaStatistic {
|
||||
movie_count: number
|
||||
// 电视剧总数
|
||||
tv_count: number
|
||||
// 电视剧总集数
|
||||
episode_count: number
|
||||
// 电视剧总集数,未获取时为 null
|
||||
episode_count: number | null
|
||||
// 用户数量
|
||||
user_count: number
|
||||
}
|
||||
@@ -1134,7 +1134,7 @@ export interface StorageConf {
|
||||
export interface MediaServerConf {
|
||||
// 名称
|
||||
name: string
|
||||
// 类型 emby/jellyfin/plex
|
||||
// 类型 emby/jellyfin/plex/trimemedia/ugreen
|
||||
type: string
|
||||
// 配置
|
||||
config: { [key: string]: any }
|
||||
@@ -1445,4 +1445,19 @@ export interface ApiResponse<T = any> {
|
||||
success: boolean
|
||||
message?: string
|
||||
data: T
|
||||
}
|
||||
}
|
||||
|
||||
// 分类规则
|
||||
export interface CategoryRule {
|
||||
genre_ids?: string
|
||||
original_language?: string
|
||||
production_countries?: string
|
||||
origin_country?: string
|
||||
release_year?: string
|
||||
}
|
||||
|
||||
// 分类配置
|
||||
export interface CategoryConfig {
|
||||
movie?: { [key: string]: CategoryRule }
|
||||
tv?: { [key: string]: CategoryRule }
|
||||
}
|
||||
|
||||
BIN
src/assets/images/logos/qq.png
Normal file
BIN
src/assets/images/logos/qq.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/images/logos/rtorrent.png
Normal file
BIN
src/assets/images/logos/rtorrent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/images/logos/ugreen.png
Normal file
BIN
src/assets/images/logos/ugreen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MediaServerPlayItem } from '@/api/types'
|
||||
import noImage from '@images/no-image.jpeg'
|
||||
import { openMediaServerWithAutoDetect } from '@/utils/appDeepLink'
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -10,12 +11,18 @@ const props = defineProps({
|
||||
|
||||
// 图片是否加载完成
|
||||
const imageLoaded = ref(false)
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// 图片加载完成响应
|
||||
function imageLoadHandler() {
|
||||
imageLoaded.value = true
|
||||
}
|
||||
|
||||
// 图片加载失败响应
|
||||
function imageErrorHandler() {
|
||||
imageLoadError.value = true
|
||||
}
|
||||
|
||||
// 跳转播放
|
||||
async function goPlay() {
|
||||
if (props.media?.link) {
|
||||
@@ -26,6 +33,7 @@ async function goPlay() {
|
||||
// 计算图片地址
|
||||
const getImgUrl = computed(() => {
|
||||
const image = props.media?.image || ''
|
||||
if (!image || imageLoadError.value) return noImage
|
||||
let url = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
|
||||
const use_cookies = props.media?.use_cookies
|
||||
if (use_cookies) {
|
||||
@@ -50,7 +58,7 @@ const getImgUrl = computed(() => {
|
||||
@click="goPlay"
|
||||
>
|
||||
<template #image>
|
||||
<VImg :src="getImgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler">
|
||||
<VImg :src="getImgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler" @error="imageErrorHandler">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
|
||||
@@ -206,6 +206,8 @@ const getIcon = computed(() => {
|
||||
return getLogoUrl('qbittorrent')
|
||||
case 'transmission':
|
||||
return getLogoUrl('transmission')
|
||||
case 'rtorrent':
|
||||
return getLogoUrl('rtorrent')
|
||||
default:
|
||||
return getLogoUrl('downloader')
|
||||
}
|
||||
@@ -443,6 +445,51 @@ onUnmounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="downloaderInfo.type == 'rtorrent'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.name"
|
||||
:label="t('downloader.name')"
|
||||
:placeholder="t('downloader.nameRequired')"
|
||||
:hint="t('downloader.name')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.host"
|
||||
:label="t('downloader.host')"
|
||||
placeholder="http(s)://ip:port/RPC2"
|
||||
:hint="t('downloader.rtorrentHostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.username"
|
||||
:label="t('downloader.username')"
|
||||
:hint="t('downloader.username')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderInfo.config.password"
|
||||
type="password"
|
||||
:label="t('downloader.password')"
|
||||
:hint="t('downloader.password')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
|
||||
@@ -33,6 +33,7 @@ function imageLoadHandler() {
|
||||
// 图片加载错误
|
||||
function imageErrorHandler() {
|
||||
imageError.value = true
|
||||
imgUrl.value = getDefaultImage()
|
||||
}
|
||||
|
||||
// 默认图片
|
||||
@@ -41,6 +42,7 @@ function getDefaultImage() {
|
||||
else if (props.media?.server_type === 'emby') return emby
|
||||
else if (props.media?.server_type === 'jellyfin') return jellyfin
|
||||
else if (props.media?.server_type === 'trimemedia') return getLogoUrl('trimemedia')
|
||||
else if (props.media?.server_type === 'ugreen') return getLogoUrl('ugreen')
|
||||
else return plex
|
||||
}
|
||||
|
||||
@@ -53,7 +55,7 @@ async function goPlay() {
|
||||
|
||||
// 生成图片代理路径
|
||||
function getImgUrl(url: string, use_cookies?: boolean) {
|
||||
if (!url) return getDefaultImage()
|
||||
if (!url || imageError.value) return getDefaultImage()
|
||||
let imgurl = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
|
||||
if (use_cookies) {
|
||||
imgurl += `&use_cookies=${encodeURIComponent(use_cookies)}`
|
||||
@@ -64,7 +66,7 @@ function getImgUrl(url: string, use_cookies?: boolean) {
|
||||
// 根据多张图片生成媒体库封面
|
||||
async function drawImages(imageList: string[], use_cookies?: boolean) {
|
||||
// 图片
|
||||
const IMAGES = imageList
|
||||
const IMAGES = [...imageList]
|
||||
if (IMAGES.length === 0) return getDefaultImage()
|
||||
|
||||
// 为所有图片添加system/img前缀
|
||||
|
||||
@@ -18,9 +18,14 @@ import { hasPermission } from '@/utils/permission'
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
interface MediaCardMedia extends MediaInfo {
|
||||
total_episode?: number
|
||||
episode_count?: number
|
||||
}
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<MediaInfo>,
|
||||
media: Object as PropType<MediaCardMedia>,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
@@ -138,7 +143,7 @@ async function handleAddSubscribe() {
|
||||
}
|
||||
|
||||
// 调用API添加订阅,电视剧的话需要指定季
|
||||
async function addSubscribe(season: number = 0, best_version: number = 0) {
|
||||
async function addSubscribe(season: number | null = null, best_version: number = 0) {
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
@@ -153,7 +158,7 @@ async function addSubscribe(season: number = 0, best_version: number = 0) {
|
||||
doubanid: props.media?.douban_id,
|
||||
bangumiid: props.media?.bangumi_id,
|
||||
mediaid: props.media?.media_id ? `${props.media?.mediaid_prefix}:${props.media?.media_id}` : '',
|
||||
season,
|
||||
season: props.media?.type === '电影' ? null : season,
|
||||
best_version,
|
||||
episode_group: episodeGroup.value,
|
||||
})
|
||||
@@ -183,8 +188,8 @@ async function addSubscribe(season: number = 0, best_version: number = 0) {
|
||||
}
|
||||
|
||||
// 弹出添加订阅提示
|
||||
function showSubscribeAddToast(result: boolean, title: string, season: number, message: string, best_version: number) {
|
||||
if (season) title = `${title} ${formatSeason(season.toString())}`
|
||||
function showSubscribeAddToast(result: boolean, title: string, season: number | null, message: string, best_version: number) {
|
||||
if (season !== null) title = `${title} ${formatSeason(season.toString())}`
|
||||
|
||||
let subname = t('subscribe.normalSub')
|
||||
if (best_version > 0) subname = t('subscribe.versionSub')
|
||||
@@ -222,7 +227,7 @@ async function removeSubscribe() {
|
||||
// 查询当前媒体是否已订阅
|
||||
async function handleCheckSubscribe() {
|
||||
try {
|
||||
const result = await checkSubscribe(props.media?.season)
|
||||
const result = await checkSubscribe(props.media?.season ?? null)
|
||||
if (result) isSubscribed.value = true
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -232,6 +237,14 @@ async function handleCheckSubscribe() {
|
||||
// 查询当前媒体是否已入库
|
||||
async function handleCheckExists() {
|
||||
try {
|
||||
// 对于总集数为 0 的电视剧季(TMDB 未返回有效集数),不展示“已入库”角标,避免误判
|
||||
const totalEpisode = props.media?.total_episode ?? props.media?.episode_count ?? props.media?.number_of_episodes ?? 0
|
||||
|
||||
if (props.media?.type === '电视剧' && totalEpisode === 0) {
|
||||
isExists.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
||||
params: {
|
||||
tmdbid: props.media?.tmdb_id,
|
||||
@@ -249,7 +262,7 @@ async function handleCheckExists() {
|
||||
}
|
||||
|
||||
// 调用API检查是否已订阅,电视剧需要指定季
|
||||
async function checkSubscribe(season = 0) {
|
||||
async function checkSubscribe(season: number | null) {
|
||||
try {
|
||||
// AbortController 现在由全局请求优化器自动管理
|
||||
const mediaid = getMediaId()
|
||||
@@ -300,7 +313,7 @@ function subscribeSeasons(seasons: MediaSeason[], seasonNoExists: { [key: number
|
||||
if (season && props.media?.tmdb_id)
|
||||
// 全部存在时洗版
|
||||
best_version = !seasonNoExists[season.season_number || 0] ? 1 : 0
|
||||
addSubscribe(season.season_number, best_version)
|
||||
addSubscribe(season.season_number ?? null, best_version)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,12 @@ const librariesOptions = ref<{ title: string; value: string | undefined }[]>([
|
||||
},
|
||||
])
|
||||
|
||||
const ugreenScanModeOptions = computed(() => [
|
||||
{ title: t('mediaserver.scanModeOptions.newAndModified'), value: 'new_and_modified' },
|
||||
{ title: t('mediaserver.scanModeOptions.supplementMissing'), value: 'supplement_missing' },
|
||||
{ title: t('mediaserver.scanModeOptions.fullOverride'), value: 'full_override' },
|
||||
])
|
||||
|
||||
// 媒体服务器详情弹窗
|
||||
const mediaServerInfoDialog = ref(false)
|
||||
|
||||
@@ -77,6 +83,15 @@ function openMediaServerInfoDialog() {
|
||||
loadLibrary(props.mediaserver.name)
|
||||
// 深复制
|
||||
mediaServerInfo.value = cloneDeep(props.mediaserver)
|
||||
if (mediaServerInfo.value.type === 'ugreen') {
|
||||
mediaServerInfo.value.config = mediaServerInfo.value.config || {}
|
||||
if (!mediaServerInfo.value.config.scan_mode) {
|
||||
mediaServerInfo.value.config.scan_mode = 'supplement_missing'
|
||||
}
|
||||
if (mediaServerInfo.value.config.verify_ssl === undefined) {
|
||||
mediaServerInfo.value.config.verify_ssl = true
|
||||
}
|
||||
}
|
||||
mediaServerInfoDialog.value = true
|
||||
if (!props.mediaserver.sync_libraries) {
|
||||
mediaServerInfo.value.sync_libraries = ['all']
|
||||
@@ -110,6 +125,8 @@ const getIcon = computed(() => {
|
||||
return getLogoUrl('jellyfin')
|
||||
case 'trimemedia':
|
||||
return getLogoUrl('trimemedia')
|
||||
case 'ugreen':
|
||||
return getLogoUrl('ugreen')
|
||||
case 'plex':
|
||||
return getLogoUrl('plex')
|
||||
default:
|
||||
@@ -196,7 +213,7 @@ onMounted(() => {
|
||||
<span class="me-2 mb-1">自定义媒体服务器</span>
|
||||
</div>
|
||||
</div>
|
||||
<VImg :src="getIcon" cover class="mt-8 me-3" max-width="3rem" min-width="3rem" />
|
||||
<VImg :src="getIcon" class="mt-8 me-3 max-h-12" max-width="3rem" min-width="3rem" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
@@ -424,6 +441,95 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="mediaServerInfo.type == 'ugreen'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaServerInfo.name"
|
||||
:label="t('common.name')"
|
||||
:placeholder="t('mediaserver.nameRequired')"
|
||||
:hint="t('mediaserver.serverAlias')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaServerInfo.config.host"
|
||||
:label="t('mediaserver.host')"
|
||||
:placeholder="t('mediaserver.hostPlaceholder')"
|
||||
:hint="t('mediaserver.hostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="mediaServerInfo.config.play_host"
|
||||
:label="t('mediaserver.playHost')"
|
||||
:placeholder="t('mediaserver.playHostPlaceholder')"
|
||||
:hint="t('mediaserver.playHostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-play-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaServerInfo.config.username"
|
||||
:label="t('mediaserver.username')"
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
type="password"
|
||||
v-model="mediaServerInfo.config.password"
|
||||
:label="t('mediaserver.password')"
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VAutocomplete
|
||||
v-model="mediaServerInfo.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
chips
|
||||
multiple
|
||||
clearable
|
||||
:hint="t('mediaserver.syncLibrariesHint')"
|
||||
persistent-hint
|
||||
active
|
||||
append-inner-icon="mdi-refresh"
|
||||
prepend-inner-icon="mdi-library"
|
||||
@click:append-inner="loadLibrary(mediaServerInfo.name)"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="mediaServerInfo.config.scan_mode"
|
||||
:label="t('mediaserver.scanMode')"
|
||||
:items="ugreenScanModeOptions"
|
||||
:hint="t('mediaserver.scanModeHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-radar"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="mediaServerInfo.config.verify_ssl"
|
||||
:label="t('mediaserver.verifySsl')"
|
||||
:hint="t('mediaserver.verifySslHint')"
|
||||
persistent-hint
|
||||
color="primary"
|
||||
inset
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="mediaServerInfo.type == 'plex'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
|
||||
@@ -102,7 +102,7 @@ function renderMarkdown(value: string) {
|
||||
</VCardTitle>
|
||||
<div
|
||||
v-if="props.message?.text && props.message?.action === 0"
|
||||
class="rounded-md text-body-1 py-1 px-4 elevation-2 bg-primary text-white chat-right mb-1"
|
||||
class="rounded-md text-body-1 py-1 px-4 elevation-2 bg-primary text-white chat-right"
|
||||
>
|
||||
<div class="markdown-body" v-html="renderMarkdown(props.message?.text)" />
|
||||
</div>
|
||||
@@ -155,12 +155,23 @@ function renderMarkdown(value: string) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
margin-block-end: 0.5rem;
|
||||
padding-inline-start: 1.5rem;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
margin-block-end: 0.5rem;
|
||||
padding-inline-start: 1.5rem;
|
||||
}
|
||||
|
||||
li {
|
||||
display: list-item;
|
||||
margin-block-end: 0.25rem;
|
||||
}
|
||||
|
||||
code {
|
||||
border-radius: 4px;
|
||||
background-color: rgba(var(--v-border-color), 0.1);
|
||||
|
||||
@@ -46,6 +46,7 @@ const notificationInfo = ref<NotificationConf>({
|
||||
const notificationTypeNames: { [key: string]: string } = {
|
||||
wechat: t('notification.wechat.name'),
|
||||
telegram: t('notification.telegram.name'),
|
||||
qqbot: t('notification.qqbot.name'),
|
||||
vocechat: t('notification.vocechat.name'),
|
||||
synologychat: t('notification.synologychat.name'),
|
||||
slack: t('notification.slack.name'),
|
||||
@@ -66,10 +67,39 @@ const notificationTypes = [
|
||||
{ value: '其它', title: t('notificationSwitch.other') },
|
||||
]
|
||||
|
||||
function ensureWechatConfigDefaults(notification: NotificationConf) {
|
||||
if (notification.type !== 'wechat') {
|
||||
return
|
||||
}
|
||||
if (!notification.config) {
|
||||
notification.config = {}
|
||||
}
|
||||
if (!notification.config.WECHAT_MODE) {
|
||||
notification.config.WECHAT_MODE = 'app'
|
||||
}
|
||||
if (!notification.config.WECHAT_BOT_WS_URL) {
|
||||
notification.config.WECHAT_BOT_WS_URL = 'wss://openws.work.weixin.qq.com'
|
||||
}
|
||||
}
|
||||
|
||||
const isWechatBotMode = computed({
|
||||
get: () => notificationInfo.value.config?.WECHAT_MODE === 'bot',
|
||||
set: value => {
|
||||
if (!notificationInfo.value.config) {
|
||||
notificationInfo.value.config = {}
|
||||
}
|
||||
notificationInfo.value.config.WECHAT_MODE = value ? 'bot' : 'app'
|
||||
if (value && !notificationInfo.value.config.WECHAT_BOT_WS_URL) {
|
||||
notificationInfo.value.config.WECHAT_BOT_WS_URL = 'wss://openws.work.weixin.qq.com'
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// 打开详情弹窗
|
||||
function openNotificationInfoDialog() {
|
||||
// 替换成深复制,避免修改时影响原数据
|
||||
notificationInfo.value = cloneDeep(props.notification)
|
||||
ensureWechatConfigDefaults(notificationInfo.value)
|
||||
notificationInfoDialog.value = true
|
||||
}
|
||||
|
||||
@@ -85,6 +115,7 @@ function saveNotificationInfo() {
|
||||
$toast.error(t('notification.channel') + `【${notificationInfo.value.name}】` + t('common.exists'))
|
||||
return
|
||||
}
|
||||
ensureWechatConfigDefaults(notificationInfo.value)
|
||||
notificationInfoDialog.value = false
|
||||
emit('change', notificationInfo.value, props.notification.name)
|
||||
emit('done')
|
||||
@@ -97,6 +128,8 @@ const getIcon = computed(() => {
|
||||
return getLogoUrl('wechat')
|
||||
case 'telegram':
|
||||
return getLogoUrl('telegram')
|
||||
case 'qqbot':
|
||||
return getLogoUrl('qq')
|
||||
case 'vocechat':
|
||||
return getLogoUrl('vocechat')
|
||||
case 'synologychat':
|
||||
@@ -187,69 +220,129 @@ function onClose() {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_CORPID"
|
||||
:label="t('notification.wechat.corpId')"
|
||||
:hint="t('notification.wechat.corpIdHint')"
|
||||
<VSwitch
|
||||
v-model="isWechatBotMode"
|
||||
:label="t('notification.wechat.useBotMode')"
|
||||
:hint="t('notification.wechat.useBotModeHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-domain"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_APP_ID"
|
||||
:label="t('notification.wechat.appId')"
|
||||
:hint="t('notification.wechat.appIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-application"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_APP_SECRET"
|
||||
:label="t('notification.wechat.appSecret')"
|
||||
:hint="t('notification.wechat.appSecretHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_PROXY"
|
||||
:label="t('notification.wechat.proxy')"
|
||||
:hint="t('notification.wechat.proxyHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-server-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_TOKEN"
|
||||
:label="t('notification.wechat.token')"
|
||||
:hint="t('notification.wechat.tokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key-variant"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_ENCODING_AESKEY"
|
||||
:label="t('notification.wechat.encodingAesKey')"
|
||||
:hint="t('notification.wechat.encodingAesKeyHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_ADMINS"
|
||||
:label="t('notification.wechat.admins')"
|
||||
:placeholder="t('notification.wechat.adminsPlaceholder')"
|
||||
:hint="t('notification.wechat.adminsHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-supervisor"
|
||||
color="primary"
|
||||
/>
|
||||
</VCol>
|
||||
<template v-if="isWechatBotMode">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_BOT_ID"
|
||||
:label="t('notification.wechat.botId')"
|
||||
:hint="t('notification.wechat.botIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-robot"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_BOT_SECRET"
|
||||
:label="t('notification.wechat.botSecret')"
|
||||
:hint="t('notification.wechat.botSecretHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_BOT_CHAT_ID"
|
||||
:label="t('notification.wechat.botChatId')"
|
||||
:placeholder="t('notification.wechat.botChatIdPlaceholder')"
|
||||
:hint="t('notification.wechat.botChatIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-chat-processing"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_BOT_WS_URL"
|
||||
:label="t('notification.wechat.botWsUrl')"
|
||||
:hint="t('notification.wechat.botWsUrlHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-lan-connect"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_ADMINS"
|
||||
:label="t('notification.wechat.admins')"
|
||||
:placeholder="t('notification.wechat.adminsPlaceholder')"
|
||||
:hint="t('notification.wechat.adminsHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-supervisor"
|
||||
/>
|
||||
</VCol>
|
||||
</template>
|
||||
<template v-else>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_CORPID"
|
||||
:label="t('notification.wechat.corpId')"
|
||||
:hint="t('notification.wechat.corpIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-domain"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_APP_ID"
|
||||
:label="t('notification.wechat.appId')"
|
||||
:hint="t('notification.wechat.appIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-application"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_APP_SECRET"
|
||||
:label="t('notification.wechat.appSecret')"
|
||||
:hint="t('notification.wechat.appSecretHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_PROXY"
|
||||
:label="t('notification.wechat.proxy')"
|
||||
:hint="t('notification.wechat.proxyHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-server-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_TOKEN"
|
||||
:label="t('notification.wechat.token')"
|
||||
:hint="t('notification.wechat.tokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key-variant"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_ENCODING_AESKEY"
|
||||
:label="t('notification.wechat.encodingAesKey')"
|
||||
:hint="t('notification.wechat.encodingAesKeyHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHAT_ADMINS"
|
||||
:label="t('notification.wechat.admins')"
|
||||
:placeholder="t('notification.wechat.adminsPlaceholder')"
|
||||
:hint="t('notification.wechat.adminsHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-supervisor"
|
||||
/>
|
||||
</VCol>
|
||||
</template>
|
||||
</VRow>
|
||||
<VRow v-else-if="notificationInfo.type == 'telegram'">
|
||||
<VCol cols="12" md="6">
|
||||
@@ -464,6 +557,56 @@ function onClose() {
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="notificationInfo.type == 'qqbot'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.name"
|
||||
:label="t('notification.name')"
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.QQ_APP_ID"
|
||||
:label="t('notification.qqbot.appId')"
|
||||
:hint="t('notification.qqbot.appIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-application"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.QQ_APP_SECRET"
|
||||
:label="t('notification.qqbot.appSecret')"
|
||||
:hint="t('notification.qqbot.appSecretHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.QQ_OPENID"
|
||||
:label="t('notification.qqbot.openId')"
|
||||
:placeholder="t('notification.qqbot.openIdPlaceholder')"
|
||||
:hint="t('notification.qqbot.openIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.QQ_GROUP_OPENID"
|
||||
:label="t('notification.qqbot.groupOpenId')"
|
||||
:placeholder="t('notification.qqbot.groupOpenIdPlaceholder')"
|
||||
:hint="t('notification.qqbot.groupOpenIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-group"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="notificationInfo.type == 'webpush'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
|
||||
663
src/components/dialog/CategoryEditDialog.vue
Normal file
663
src/components/dialog/CategoryEditDialog.vue
Normal file
@@ -0,0 +1,663 @@
|
||||
<script setup lang="ts">
|
||||
import draggable from 'vuedraggable'
|
||||
import api from '@/api'
|
||||
import type { CategoryConfig } from '@/api/types'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 定义输入参数
|
||||
defineProps<{
|
||||
modelValue?: boolean
|
||||
}>()
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
const activeTab = ref('movie')
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const toast = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const generateId = () => {
|
||||
return 'id-' + Math.random().toString(36).substr(2, 9) + '-' + Date.now()
|
||||
}
|
||||
|
||||
interface CategoryItem {
|
||||
id: string
|
||||
name: string
|
||||
rule: any
|
||||
}
|
||||
|
||||
const movieList = ref<CategoryItem[]>([])
|
||||
const tvList = ref<CategoryItem[]>([])
|
||||
|
||||
// TMDB 类型映射
|
||||
const genreOptions = [
|
||||
{ title: '动作 (Action)', value: '28' },
|
||||
{ title: '冒险 (Adventure)', value: '12' },
|
||||
{ title: '动画 (Animation)', value: '16' },
|
||||
{ title: '喜剧 (Comedy)', value: '35' },
|
||||
{ title: '犯罪 (Crime)', value: '80' },
|
||||
{ title: '纪录 (Documentary)', value: '99' },
|
||||
{ title: '剧情 (Drama)', value: '18' },
|
||||
{ title: '家庭 (Family)', value: '10751' },
|
||||
{ title: '奇幻 (Fantasy)', value: '14' },
|
||||
{ title: '历史 (History)', value: '36' },
|
||||
{ title: '恐怖 (Horror)', value: '27' },
|
||||
{ title: '音乐 (Music)', value: '10402' },
|
||||
{ title: '悬疑 (Mystery)', value: '9648' },
|
||||
{ title: '爱情 (Romance)', value: '10749' },
|
||||
{ title: '科幻 (SF)', value: '878' },
|
||||
{ title: '电视电影', value: '10770' },
|
||||
{ title: '惊悚 (Thriller)', value: '53' },
|
||||
{ title: '战争 (War)', value: '10752' },
|
||||
{ title: '西部 (Western)', value: '37' },
|
||||
{ title: '儿童 (Kids)', value: '10762' },
|
||||
{ title: '新闻 (News)', value: '10763' },
|
||||
{ title: '真人秀 (Reality)', value: '10764' },
|
||||
{ title: '科幻/奇幻 (Sci-Fi)', value: '10765' },
|
||||
{ title: '肥皂剧 (Soap)', value: '10766' },
|
||||
{ title: '访谈 (Talk)', value: '10767' },
|
||||
{ title: '战争/政治', value: '10768' },
|
||||
]
|
||||
|
||||
// 语种选项 (original_language)
|
||||
const languageOptions = [
|
||||
{ title: '中文', value: 'zh' },
|
||||
{ title: '中文', value: 'cn' },
|
||||
{ title: '英语 (English)', value: 'en' },
|
||||
{ title: '日语 (Japanese)', value: 'ja' },
|
||||
{ title: '韩语 (Korean)', value: 'ko' },
|
||||
{ title: '法语 (French)', value: 'fr' },
|
||||
{ title: '德语 (German)', value: 'de' },
|
||||
{ title: '西班牙语 (Spanish)', value: 'es' },
|
||||
{ title: '意大利语 (Italian)', value: 'it' },
|
||||
{ title: '葡萄牙语 (Portuguese)', value: 'pt' },
|
||||
{ title: '俄语 (Russian)', value: 'ru' },
|
||||
{ title: '阿拉伯语', value: 'ar' },
|
||||
{ title: '泰语 (Thai)', value: 'th' },
|
||||
{ title: '越南语 (Vietnamese)', value: 'vi' },
|
||||
{ title: '印地语 (Hindi)', value: 'hi' },
|
||||
{ title: '土耳其语 (Turkish)', value: 'tr' },
|
||||
{ title: '荷兰语 (Dutch)', value: 'nl' },
|
||||
{ title: '波兰语 (Polish)', value: 'pl' },
|
||||
{ title: '瑞典语 (Swedish)', value: 'sv' },
|
||||
{ title: '丹麦语 (Danish)', value: 'da' },
|
||||
{ title: '挪威语 (Norwegian)', value: 'nb' },
|
||||
{ title: '芬兰语 (Finnish)', value: 'fi' },
|
||||
{ title: '希腊语 (Greek)', value: 'el' },
|
||||
{ title: '捷克语 (Czech)', value: 'cs' },
|
||||
{ title: '匈牙利语 (Hungarian)', value: 'hu' },
|
||||
{ title: '罗马尼亚语 (Romanian)', value: 'ro' },
|
||||
{ title: '乌克兰语 (Ukrainian)', value: 'uk' },
|
||||
{ title: '印度尼西亚语 (Indonesian)', value: 'id' },
|
||||
{ title: '马来语 (Malay)', value: 'ms' },
|
||||
{ title: '希伯来语 (Hebrew)', value: 'he' },
|
||||
]
|
||||
|
||||
// 国家/地区选项 (origin_country/production_countries)
|
||||
const countryOptions = [
|
||||
{ title: '中国大陆 (CN)', value: 'CN' },
|
||||
{ title: '中国香港 (HK)', value: 'HK' },
|
||||
{ title: '中国台湾 (TW)', value: 'TW' },
|
||||
{ title: '美国 (US)', value: 'US' },
|
||||
{ title: '英国 (GB)', value: 'GB' },
|
||||
{ title: '日本 (JP)', value: 'JP' },
|
||||
{ title: '韩国 (KR)', value: 'KR' },
|
||||
{ title: '法国 (FR)', value: 'FR' },
|
||||
{ title: '德国 (DE)', value: 'DE' },
|
||||
{ title: '意大利 (IT)', value: 'IT' },
|
||||
{ title: '西班牙 (ES)', value: 'ES' },
|
||||
{ title: '加拿大 (CA)', value: 'CA' },
|
||||
{ title: '澳大利亚 (AU)', value: 'AU' },
|
||||
{ title: '俄罗斯 (RU)', value: 'RU' },
|
||||
{ title: '印度 (IN)', value: 'IN' },
|
||||
{ title: '泰国 (TH)', value: 'TH' },
|
||||
{ title: '新加坡 (SG)', value: 'SG' },
|
||||
{ title: '马来西亚 (MY)', value: 'MY' },
|
||||
{ title: '越南 (VN)', value: 'VN' },
|
||||
{ title: '菲律宾 (PH)', value: 'PH' },
|
||||
{ title: '巴西 (BR)', value: 'BR' },
|
||||
{ title: '墨西哥 (MX)', value: 'MX' },
|
||||
{ title: '阿根廷 (AR)', value: 'AR' },
|
||||
{ title: '荷兰 (NL)', value: 'NL' },
|
||||
{ title: '比利时 (BE)', value: 'BE' },
|
||||
{ title: '瑞士 (CH)', value: 'CH' },
|
||||
{ title: '瑞典 (SE)', value: 'SE' },
|
||||
{ title: '挪威 (NO)', value: 'NO' },
|
||||
{ title: '丹麦 (DK)', value: 'DK' },
|
||||
{ title: '波兰 (PL)', value: 'PL' },
|
||||
{ title: '捷克 (CZ)', value: 'CZ' },
|
||||
{ title: '土耳其 (TR)', value: 'TR' },
|
||||
{ title: '以色列 (IL)', value: 'IL' },
|
||||
{ title: '埃及 (EG)', value: 'EG' },
|
||||
{ title: '南非 (ZA)', value: 'ZA' },
|
||||
{ title: '新西兰 (NZ)', value: 'NZ' },
|
||||
]
|
||||
|
||||
const fetchConfig = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await api.get('media/category/config')
|
||||
if (res && res.data) {
|
||||
parseConfig(res.data)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error(t('setting.category.loadFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const parseConfig = (data: CategoryConfig) => {
|
||||
// 将对象 { "Name": { ... } } 转换为数组 [ { id: uuid, name: "Name", rule: { ... } } ]
|
||||
movieList.value = []
|
||||
if (data.movie) {
|
||||
for (const [key, value] of Object.entries(data.movie)) {
|
||||
// 为了UI一致性处理 genre_ids 为数组或字符串,但 API 发送的是字符串
|
||||
const rule = { ...value }
|
||||
if (rule.genre_ids && typeof rule.genre_ids === 'string') {
|
||||
// UI 多选预期为数组,检查输入。实际上 VAutocomplete 多选预期数组。我们需要将字符串分割为数组。
|
||||
// @ts-ignore
|
||||
rule.genre_ids = rule.genre_ids.split(',')
|
||||
} else {
|
||||
// @ts-ignore
|
||||
rule.genre_ids = []
|
||||
}
|
||||
|
||||
// 处理语种
|
||||
if (rule.original_language && typeof rule.original_language === 'string') {
|
||||
// @ts-ignore
|
||||
rule.original_language = rule.original_language.split(',')
|
||||
} else {
|
||||
// @ts-ignore
|
||||
rule.original_language = []
|
||||
}
|
||||
|
||||
// 处理制片国家/地区
|
||||
if (rule.production_countries && typeof rule.production_countries === 'string') {
|
||||
// @ts-ignore
|
||||
rule.production_countries = rule.production_countries.split(',')
|
||||
} else {
|
||||
// @ts-ignore
|
||||
rule.production_countries = []
|
||||
}
|
||||
|
||||
movieList.value.push({
|
||||
id: generateId(),
|
||||
name: key,
|
||||
rule: rule as any,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
tvList.value = []
|
||||
if (data.tv) {
|
||||
for (const [key, value] of Object.entries(data.tv)) {
|
||||
const rule = { ...value }
|
||||
if (rule.genre_ids && typeof rule.genre_ids === 'string') {
|
||||
// @ts-ignore
|
||||
rule.genre_ids = rule.genre_ids.split(',')
|
||||
} else {
|
||||
// @ts-ignore
|
||||
rule.genre_ids = []
|
||||
}
|
||||
|
||||
// 处理语种
|
||||
if (rule.original_language && typeof rule.original_language === 'string') {
|
||||
// @ts-ignore
|
||||
rule.original_language = rule.original_language.split(',')
|
||||
} else {
|
||||
// @ts-ignore
|
||||
rule.original_language = []
|
||||
}
|
||||
|
||||
// 处理发行国家/地区
|
||||
if (rule.origin_country && typeof rule.origin_country === 'string') {
|
||||
// @ts-ignore
|
||||
rule.origin_country = rule.origin_country.split(',')
|
||||
} else {
|
||||
// @ts-ignore
|
||||
rule.origin_country = []
|
||||
}
|
||||
|
||||
tvList.value.push({
|
||||
id: generateId(),
|
||||
name: key,
|
||||
rule: rule as any,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const addMovieItem = () => {
|
||||
movieList.value.push({
|
||||
id: generateId(),
|
||||
name: '新分类',
|
||||
rule: { genre_ids: [] as any },
|
||||
})
|
||||
}
|
||||
|
||||
const removeMovieItem = (index: number) => {
|
||||
movieList.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const addTvItem = () => {
|
||||
tvList.value.push({
|
||||
id: generateId(),
|
||||
name: '新分类',
|
||||
rule: { genre_ids: [] as any },
|
||||
})
|
||||
}
|
||||
|
||||
const removeTvItem = (index: number) => {
|
||||
tvList.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const saveConfig = async () => {
|
||||
saving.value = true
|
||||
try {
|
||||
// 将数组转换回对象
|
||||
const payload: CategoryConfig = {
|
||||
movie: {},
|
||||
tv: {},
|
||||
}
|
||||
|
||||
movieList.value.forEach(item => {
|
||||
if (item.name) {
|
||||
const rule = { ...item.rule }
|
||||
// 将 genre_ids 数组转换回字符串
|
||||
if (Array.isArray(rule.genre_ids) && rule.genre_ids.length > 0) {
|
||||
rule.genre_ids = rule.genre_ids.join(',')
|
||||
} else {
|
||||
// @ts-ignore
|
||||
rule.genre_ids = null
|
||||
}
|
||||
|
||||
// 将 original_language 数组转换回字符串
|
||||
if (Array.isArray(rule.original_language) && rule.original_language.length > 0) {
|
||||
rule.original_language = rule.original_language.join(',')
|
||||
} else {
|
||||
rule.original_language = undefined
|
||||
}
|
||||
|
||||
// 将 production_countries 数组转换回字符串
|
||||
if (Array.isArray(rule.production_countries) && rule.production_countries.length > 0) {
|
||||
rule.production_countries = rule.production_countries.join(',')
|
||||
} else {
|
||||
rule.production_countries = undefined
|
||||
}
|
||||
|
||||
// 清理空字符串
|
||||
if (!rule.release_year) rule.release_year = undefined
|
||||
|
||||
// @ts-ignore
|
||||
payload.movie[item.name] = rule
|
||||
}
|
||||
})
|
||||
|
||||
tvList.value.forEach(item => {
|
||||
if (item.name) {
|
||||
const rule = { ...item.rule }
|
||||
if (Array.isArray(rule.genre_ids) && rule.genre_ids.length > 0) {
|
||||
rule.genre_ids = rule.genre_ids.join(',')
|
||||
} else {
|
||||
// @ts-ignore
|
||||
rule.genre_ids = null
|
||||
}
|
||||
|
||||
// 将 original_language 数组转换回字符串
|
||||
if (Array.isArray(rule.original_language) && rule.original_language.length > 0) {
|
||||
rule.original_language = rule.original_language.join(',')
|
||||
} else {
|
||||
rule.original_language = undefined
|
||||
}
|
||||
|
||||
// 将 origin_country 数组转换回字符串
|
||||
if (Array.isArray(rule.origin_country) && rule.origin_country.length > 0) {
|
||||
rule.origin_country = rule.origin_country.join(',')
|
||||
} else {
|
||||
rule.origin_country = undefined
|
||||
}
|
||||
|
||||
// 清理空字符串
|
||||
if (!rule.release_year) rule.release_year = undefined
|
||||
|
||||
// @ts-ignore
|
||||
payload.tv[item.name] = rule
|
||||
}
|
||||
})
|
||||
|
||||
const res: any = await api.post('media/category/config', payload)
|
||||
if (res && res.success) {
|
||||
toast.success(t('setting.category.saveSuccess'))
|
||||
emit('save')
|
||||
emit('close')
|
||||
} else {
|
||||
toast.error(t('setting.category.saveFailed', { message: res.message || 'Error' }))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error(t('setting.category.saveFailed', { message: 'Network or Config Error' }))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog :model-value="modelValue" max-width="1000" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardItem class="py-3">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-shape-outline" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>
|
||||
{{ t('setting.category.title') }}
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>
|
||||
{{ t('setting.category.subtitle') }}
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VTabs v-model="activeTab" show-arrows class="mb-4">
|
||||
<VTab value="movie">
|
||||
<VIcon icon="mdi-movie-outline" class="me-2" />
|
||||
{{ t('setting.category.movie') }}
|
||||
</VTab>
|
||||
<VTab value="tv">
|
||||
<VIcon icon="mdi-television" class="me-2" />
|
||||
{{ t('setting.category.tv') }}
|
||||
</VTab>
|
||||
</VTabs>
|
||||
|
||||
<div v-if="loading" class="d-flex justify-center align-center" style="min-height: 300px">
|
||||
<VProgressCircular indeterminate color="primary" size="64" />
|
||||
</div>
|
||||
|
||||
<VWindow v-else v-model="activeTab" class="disable-tab-transition" :touch="false">
|
||||
<VWindowItem value="movie">
|
||||
<draggable v-model="movieList" handle=".drag-handle" item-key="id" animation="200">
|
||||
<template #item="{ element, index }">
|
||||
<VCard variant="tonal" class="mb-4 category-item">
|
||||
<VCardText class="pa-4">
|
||||
<div class="d-flex align-center mb-5">
|
||||
<VTextField
|
||||
v-model="element.name"
|
||||
:label="t('setting.category.name')"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
variant="plain"
|
||||
class="font-bold"
|
||||
prepend-inner-icon="mdi-tag-outline"
|
||||
/>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
icon="mdi-drag-vertical"
|
||||
variant="text"
|
||||
size="small"
|
||||
class="drag-handle me-2"
|
||||
color="primary"
|
||||
/>
|
||||
<VBtn
|
||||
icon="mdi-delete-outline"
|
||||
color="error"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="removeMovieItem(index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VAutocomplete
|
||||
v-model="element.rule.genre_ids"
|
||||
:items="genreOptions"
|
||||
:label="t('setting.category.genre')"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
multiple
|
||||
chips
|
||||
closable-chips
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-movie-filter-outline"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VAutocomplete
|
||||
v-model="element.rule.production_countries"
|
||||
:items="countryOptions"
|
||||
:label="t('setting.category.country')"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
multiple
|
||||
chips
|
||||
closable-chips
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-earth"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VAutocomplete
|
||||
v-model="element.rule.original_language"
|
||||
:items="languageOptions"
|
||||
:label="t('setting.category.language')"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
multiple
|
||||
chips
|
||||
closable-chips
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-translate"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="element.rule.release_year"
|
||||
:label="t('setting.category.year')"
|
||||
:placeholder="t('setting.category.yearPlaceholder')"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-calendar-range"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<VBtn
|
||||
block
|
||||
variant="outlined"
|
||||
size="large"
|
||||
prepend-icon="mdi-plus-circle-outline"
|
||||
class="mt-2 add-category-btn"
|
||||
@click="addMovieItem"
|
||||
>
|
||||
{{ t('setting.category.addMovie') }}
|
||||
</VBtn>
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem value="tv">
|
||||
<draggable v-model="tvList" handle=".drag-handle" item-key="id" animation="200">
|
||||
<template #item="{ element, index }">
|
||||
<VCard variant="tonal" class="mb-4 category-item">
|
||||
<VCardText class="pa-4">
|
||||
<div class="d-flex align-center mb-5">
|
||||
<VTextField
|
||||
v-model="element.name"
|
||||
:label="t('setting.category.name')"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
variant="plain"
|
||||
class="font-bold"
|
||||
prepend-inner-icon="mdi-tag-outline"
|
||||
/>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
icon="mdi-drag-vertical"
|
||||
variant="text"
|
||||
size="small"
|
||||
class="drag-handle me-2"
|
||||
color="primary"
|
||||
/>
|
||||
<VBtn
|
||||
icon="mdi-delete-outline"
|
||||
color="error"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="removeTvItem(index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VAutocomplete
|
||||
v-model="element.rule.genre_ids"
|
||||
:items="genreOptions"
|
||||
:label="t('setting.category.genre')"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
multiple
|
||||
chips
|
||||
closable-chips
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-movie-filter-outline"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VAutocomplete
|
||||
v-model="element.rule.origin_country"
|
||||
:items="countryOptions"
|
||||
:label="t('setting.category.country')"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
multiple
|
||||
chips
|
||||
closable-chips
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-earth"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VAutocomplete
|
||||
v-model="element.rule.original_language"
|
||||
:items="languageOptions"
|
||||
:label="t('setting.category.language')"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
multiple
|
||||
chips
|
||||
closable-chips
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-translate"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="element.rule.release_year"
|
||||
:label="t('setting.category.year')"
|
||||
:placeholder="t('setting.category.yearPlaceholder')"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-calendar-range"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<VBtn
|
||||
block
|
||||
variant="outlined"
|
||||
size="large"
|
||||
prepend-icon="mdi-plus-circle-outline"
|
||||
class="mt-2 add-category-btn"
|
||||
@click="addTvItem"
|
||||
>
|
||||
{{ t('setting.category.addTv') }}
|
||||
</VBtn>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions class="pt-3">
|
||||
<VSpacer />
|
||||
<VBtn variant="text" @click="emit('close')">
|
||||
{{ t('common.cancel') }}
|
||||
</VBtn>
|
||||
<VBtn color="primary" :loading="saving" prepend-icon="mdi-content-save" class="px-5" @click="saveConfig">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.drag-handle:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.category-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.add-category-btn {
|
||||
border-style: dashed !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.add-category-btn:hover {
|
||||
border-style: solid !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.disable-tab-transition > * {
|
||||
transition: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -10,6 +10,7 @@ import { FileItem, StorageConf, TransferDirectoryConf, TransferForm } from '@/ap
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalSettingsStore } from '@/stores'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -63,6 +64,9 @@ const progressText = ref(t('dialog.reorganize.processing'))
|
||||
// 整理进度
|
||||
const progressValue = ref(0)
|
||||
|
||||
// 进度SSE连接
|
||||
const progressSSE = ref<any>(null)
|
||||
|
||||
// 所有存储
|
||||
const storages = ref<StorageConf[]>([])
|
||||
|
||||
@@ -200,25 +204,31 @@ function handleProgressMessage(event: MessageEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优化的进度SSE连接
|
||||
const progressSSE = useProgressSSE(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer`,
|
||||
handleProgressMessage,
|
||||
'reorganize-progress',
|
||||
progressActive,
|
||||
)
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
function startLoadingProgress() {
|
||||
function startLoadingProgress(key: string) {
|
||||
progressText.value = t('dialog.reorganize.processing')
|
||||
progressActive.value = true
|
||||
progressSSE.start()
|
||||
|
||||
// 如果已经有连接,先停止
|
||||
if (progressSSE.value) {
|
||||
progressSSE.value.stop()
|
||||
}
|
||||
|
||||
const url = `${import.meta.env.VITE_API_BASE_URL}system/progress/${key}`
|
||||
|
||||
// 创建新的SSE连接
|
||||
progressSSE.value = useProgressSSE(url, handleProgressMessage, `reorganize-progress-${key}`, progressActive)
|
||||
|
||||
progressSSE.value.start()
|
||||
}
|
||||
|
||||
// 停止监听加载进度
|
||||
function stopLoadingProgress() {
|
||||
progressActive.value = false
|
||||
progressSSE.stop()
|
||||
if (progressSSE.value) {
|
||||
progressSSE.value.stop()
|
||||
progressSSE.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 整理文件
|
||||
@@ -228,25 +238,30 @@ async function transfer(background: boolean = false) {
|
||||
// 显示进度条
|
||||
progressDialog.value = true
|
||||
|
||||
if (!background) {
|
||||
// 开始监听进度
|
||||
startLoadingProgress()
|
||||
}
|
||||
|
||||
// 文件整理
|
||||
if (props.items) {
|
||||
for (const item of props.items) {
|
||||
if (!background) {
|
||||
// 如果是文件,计算MD5
|
||||
const key = item.type === 'dir' ? 'filetransfer' : CryptoJS.MD5(item.path).toString()
|
||||
|
||||
// 开始监听进度
|
||||
startLoadingProgress(key)
|
||||
}
|
||||
await handleTransfer(item, background)
|
||||
}
|
||||
}
|
||||
|
||||
// 日志整理
|
||||
if (props.logids) {
|
||||
if (!background) {
|
||||
// 为日志整理任务开启进度监听
|
||||
startLoadingProgress('filetransfer')
|
||||
}
|
||||
for (const logid of props.logids) {
|
||||
await handleTransferLog(logid, background)
|
||||
}
|
||||
}
|
||||
|
||||
if (!background) {
|
||||
// 停止监听进度
|
||||
stopLoadingProgress()
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import QRCode from 'qrcode'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 常量定义
|
||||
const AUTH_WINDOW_WIDTH = 600
|
||||
const AUTH_WINDOW_HEIGHT = 700
|
||||
const POLL_INTERVAL = 2000
|
||||
const AUTH_STATUS_SUCCESS = 2
|
||||
const AUTH_STATUS_FAILED = -1
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
|
||||
// 定义输入
|
||||
// Props 定义
|
||||
const props = defineProps({
|
||||
conf: {
|
||||
type: Object as PropType<{ [key: string]: any }>,
|
||||
@@ -18,27 +24,40 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
// Events 定义
|
||||
const emit = defineEmits(['done', 'close'])
|
||||
|
||||
// 二维码内容
|
||||
const qrCodeContent = ref('')
|
||||
// 响应式状态
|
||||
const authUrl = ref('')
|
||||
const authState = ref('')
|
||||
const text = ref('')
|
||||
const alertType = ref<'success' | 'info' | 'error' | 'warning'>('info')
|
||||
|
||||
// 二维码图片 base64
|
||||
const qrCodeImage = ref('')
|
||||
// 授权窗口引用
|
||||
let authWindow: Window | null = null
|
||||
let pollTimer: NodeJS.Timeout | undefined
|
||||
|
||||
// 下方的提示信息
|
||||
const text = ref(t('dialog.u115Auth.scanQrCode'))
|
||||
// 清理资源
|
||||
function cleanup() {
|
||||
if (pollTimer) {
|
||||
clearTimeout(pollTimer)
|
||||
pollTimer = undefined
|
||||
}
|
||||
if (authWindow && !authWindow.closed) {
|
||||
authWindow.close()
|
||||
authWindow = null
|
||||
}
|
||||
}
|
||||
|
||||
// 提醒类型
|
||||
const alertType = ref<'success' | 'info' | 'error' | 'warning' | undefined>('info')
|
||||
// 设置提示消息
|
||||
function setMessage(type: typeof alertType.value, message: string) {
|
||||
alertType.value = type
|
||||
text.value = message
|
||||
}
|
||||
|
||||
// timeout定时器
|
||||
let timeoutTimer: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
// 完成
|
||||
async function handleDone() {
|
||||
clearTimeout(timeoutTimer)
|
||||
// 完成授权
|
||||
function handleDone() {
|
||||
cleanup()
|
||||
emit('done')
|
||||
}
|
||||
|
||||
@@ -47,78 +66,118 @@ async function handleReset() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/storage/reset/u115')
|
||||
if (result.success) {
|
||||
// 重置成功
|
||||
alertType.value = 'success'
|
||||
setMessage('success', t('dialog.u115Auth.authSuccess'))
|
||||
handleDone()
|
||||
} else {
|
||||
alertType.value = 'error'
|
||||
text.value = result.message
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
else {
|
||||
setMessage('error', result.message || t('dialog.u115Auth.authFailed'))
|
||||
}
|
||||
}
|
||||
}
|
||||
// 调用/u115/qrcode api生成二维码
|
||||
async function getQrcode() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/storage/qrcode/u115')
|
||||
if (result.success && result.data) {
|
||||
qrCodeContent.value = result.data.codeContent
|
||||
// 生成二维码图片
|
||||
qrCodeImage.value = await QRCode.toDataURL(result.data.codeContent, {
|
||||
width: 200,
|
||||
margin: 1,
|
||||
})
|
||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||
} else {
|
||||
text.value = result.message
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
catch (error) {
|
||||
console.error('Reset failed:', error)
|
||||
setMessage('error', t('dialog.u115Auth.authFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
// 调用/aliyun/check api验证二维码
|
||||
async function checkQrcode() {
|
||||
// 获取授权URL
|
||||
async function fetchAuthUrl() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/storage/auth_url/u115')
|
||||
|
||||
if (result.success && result.data) {
|
||||
authUrl.value = result.data.authUrl
|
||||
authState.value = result.data.state
|
||||
}
|
||||
else {
|
||||
setMessage('error', result.message || t('dialog.u115Auth.urlFetchFailed'))
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Fetch auth URL failed:', error)
|
||||
setMessage('error', t('dialog.u115Auth.urlFetchFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
// 打开授权窗口
|
||||
function openAuthWindow() {
|
||||
if (!authUrl.value) {
|
||||
setMessage('error', t('dialog.u115Auth.urlEmpty'))
|
||||
return
|
||||
}
|
||||
|
||||
const left = (window.screen.width - AUTH_WINDOW_WIDTH) / 2
|
||||
const top = (window.screen.height - AUTH_WINDOW_HEIGHT) / 2
|
||||
const features = [
|
||||
`width=${AUTH_WINDOW_WIDTH}`,
|
||||
`height=${AUTH_WINDOW_HEIGHT}`,
|
||||
`left=${left}`,
|
||||
`top=${top}`,
|
||||
'toolbar=no',
|
||||
'location=no',
|
||||
'status=no',
|
||||
'menubar=no',
|
||||
'scrollbars=yes',
|
||||
'resizable=yes',
|
||||
].join(',')
|
||||
|
||||
authWindow = window.open(authUrl.value, '115授权', features)
|
||||
|
||||
if (authWindow) {
|
||||
setMessage('info', t('dialog.u115Auth.authorizing'))
|
||||
pollTimer = setTimeout(checkAuthStatus, POLL_INTERVAL)
|
||||
}
|
||||
else {
|
||||
setMessage('error', t('dialog.u115Auth.popupBlocked'))
|
||||
}
|
||||
}
|
||||
|
||||
// 检查授权状态
|
||||
async function checkAuthStatus() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/storage/check/u115')
|
||||
|
||||
if (result.success && result.data) {
|
||||
const status = result.data.status
|
||||
text.value = result.data.tip
|
||||
if (status == 0) {
|
||||
alertType.value = 'info'
|
||||
// 新建、待扫码
|
||||
clearTimeout(timeoutTimer)
|
||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||
} else if (status == 1) {
|
||||
// 已扫码
|
||||
alertType.value = 'info'
|
||||
text.value = t('dialog.u115Auth.scanned')
|
||||
clearTimeout(timeoutTimer)
|
||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||
} else if (status == 2) {
|
||||
// 已确认完成
|
||||
alertType.value = 'success'
|
||||
const { status, tip } = result.data
|
||||
|
||||
if (status === AUTH_STATUS_SUCCESS) {
|
||||
// 授权成功
|
||||
setMessage('success', t('dialog.u115Auth.authSuccess'))
|
||||
handleDone()
|
||||
} else {
|
||||
// 过期或者已取消
|
||||
alertType.value = 'error'
|
||||
return
|
||||
}
|
||||
} else {
|
||||
alertType.value = 'error'
|
||||
text.value = result.message
|
||||
|
||||
if (status === AUTH_STATUS_FAILED) {
|
||||
// 授权失败或过期
|
||||
setMessage('error', tip || t('dialog.u115Auth.authFailed'))
|
||||
cleanup()
|
||||
return
|
||||
}
|
||||
|
||||
// status === 0 或 1,继续等待
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Check auth status failed:', error)
|
||||
}
|
||||
|
||||
// 检查窗口是否被用户关闭
|
||||
if (authWindow?.closed) {
|
||||
setMessage('warning', t('dialog.u115Auth.authCanceled'))
|
||||
cleanup()
|
||||
return
|
||||
}
|
||||
|
||||
// 继续轮询
|
||||
pollTimer = setTimeout(checkAuthStatus, POLL_INTERVAL)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getQrcode()
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
fetchAuthUrl()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeoutTimer) clearTimeout(timeoutTimer)
|
||||
cleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -126,37 +185,63 @@ onUnmounted(() => {
|
||||
<VDialog width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-qrcode" class="me-2" />
|
||||
<VIcon icon="mdi-shield-key" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>
|
||||
{{ t('dialog.u115Auth.loginTitle') }}
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<VCardText class="pt-2 flex flex-col items-center justify-center">
|
||||
<div class="mt-6 rounded text-center p-3 border">
|
||||
<VImg class="mx-auto" :src="qrCodeImage" width="200" height="200">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-1 aspect-h-1" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
<!-- 授权按钮 -->
|
||||
<div class="mt-6 mb-4 text-center">
|
||||
<VBtn
|
||||
size="x-large"
|
||||
color="primary"
|
||||
prepend-icon="mdi-login"
|
||||
:disabled="!authUrl"
|
||||
class="px-8"
|
||||
@click="openAuthWindow"
|
||||
>
|
||||
{{ t('dialog.u115Auth.openAuthWindow') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
<div>
|
||||
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
|
||||
|
||||
<!-- 状态提示 -->
|
||||
<div v-if="text" class="w-full">
|
||||
<VAlert
|
||||
variant="tonal"
|
||||
:type="alertType"
|
||||
:text="text"
|
||||
class="my-4 text-center"
|
||||
>
|
||||
<template #prepend />
|
||||
</VAlert>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VBtn color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
|
||||
<VBtn
|
||||
color="error"
|
||||
prepend-icon="mdi-restore"
|
||||
class="px-5 me-3"
|
||||
@click="handleReset"
|
||||
>
|
||||
{{ t('dialog.u115Auth.reset') }}
|
||||
</VBtn>
|
||||
|
||||
<VSpacer />
|
||||
<VBtn @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
|
||||
|
||||
<VBtn
|
||||
prepend-icon="mdi-check"
|
||||
class="px-5 me-3"
|
||||
@click="handleDone"
|
||||
>
|
||||
{{ t('dialog.u115Auth.complete') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
@@ -199,6 +199,7 @@ async function fetchUserInfo() {
|
||||
userForm.value = await api.get(`user/${props.username}`)
|
||||
if (userForm.value) {
|
||||
userForm.value.avatar = userForm.value.avatar || avatar1
|
||||
userForm.value.nickname = userForm.value.settings?.nickname ?? ''
|
||||
currentAvatar.value = userForm.value.avatar
|
||||
currentUserName.value = userForm.value.name
|
||||
userName.value = userForm.value.name
|
||||
@@ -273,12 +274,10 @@ async function updateUser() {
|
||||
}
|
||||
|
||||
// 将nickname保存到settings中,后端可以直接处理JSON对象
|
||||
if (userForm.value.nickname) {
|
||||
if (!userForm.value.settings) {
|
||||
userForm.value.settings = {}
|
||||
}
|
||||
userForm.value.settings.nickname = userForm.value.nickname
|
||||
if (!userForm.value.settings) {
|
||||
userForm.value.settings = {}
|
||||
}
|
||||
userForm.value.settings.nickname = userForm.value.nickname ?? ''
|
||||
|
||||
const oldUserName = userForm.value.name
|
||||
userForm.value.name = currentUserName.value
|
||||
|
||||
@@ -188,6 +188,7 @@ export function useSetupWizard() {
|
||||
'jellyfin': 'JellyfinModule',
|
||||
'plex': 'PlexModule',
|
||||
'trimemedia': 'TrimeMediaModule',
|
||||
'ugreen': 'UgreenModule',
|
||||
},
|
||||
// 通知映射
|
||||
notification: {
|
||||
@@ -405,7 +406,7 @@ export function useSetupWizard() {
|
||||
errors.push(t('mediaserver.tokenRequired'))
|
||||
validationErrors.value.mediaServer.token = true
|
||||
}
|
||||
} else if (wizardData.value.mediaServer.type === 'trimemedia') {
|
||||
} else if (wizardData.value.mediaServer.type === 'trimemedia' || wizardData.value.mediaServer.type === 'ugreen') {
|
||||
if (!wizardData.value.mediaServer.config?.username?.trim()) {
|
||||
errors.push(t('mediaserver.usernameRequired'))
|
||||
validationErrors.value.mediaServer.username = true
|
||||
@@ -486,6 +487,16 @@ export function useSetupWizard() {
|
||||
validationErrors.value.notification.VOCECHAT_API_KEY = true
|
||||
}
|
||||
break
|
||||
case 'qqbot':
|
||||
if (!config.QQ_APP_ID?.trim()) {
|
||||
errors.push(t('notification.qqbot.appIdRequired'))
|
||||
validationErrors.value.notification.QQ_APP_ID = true
|
||||
}
|
||||
if (!config.QQ_APP_SECRET?.trim()) {
|
||||
errors.push(t('notification.qqbot.appSecretRequired'))
|
||||
validationErrors.value.notification.QQ_APP_SECRET = true
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -7,6 +7,7 @@ import ModuleTestView from '@/views/system/ModuleTestView.vue'
|
||||
import MessageView from '@/views/system/MessageView.vue'
|
||||
import WordsView from '@/views/system/WordsView.vue'
|
||||
import CacheView from '@/views/system/CacheView.vue'
|
||||
import AccountSettingService from '@/views/system/ServiceView.vue'
|
||||
import api from '@/api'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { getQueryValue } from '@/@core/utils'
|
||||
@@ -49,6 +50,9 @@ const wordsDialog = ref(false)
|
||||
// 缓存管理弹窗
|
||||
const cacheDialog = ref(false)
|
||||
|
||||
// 定时服务弹窗
|
||||
const schedulerDialog = ref(false)
|
||||
|
||||
// 输入消息
|
||||
const user_message = ref('')
|
||||
|
||||
@@ -108,6 +112,13 @@ const shortcuts = [
|
||||
dialog: 'cache',
|
||||
dialogRef: cacheDialog,
|
||||
},
|
||||
{
|
||||
title: t('shortcut.scheduler.title'),
|
||||
subtitle: t('shortcut.scheduler.subtitle'),
|
||||
icon: 'mdi-list-box',
|
||||
dialog: 'scheduler',
|
||||
dialogRef: schedulerDialog,
|
||||
},
|
||||
{
|
||||
title: t('shortcut.system.title'),
|
||||
subtitle: t('shortcut.system.subtitle'),
|
||||
@@ -275,10 +286,10 @@ onMounted(() => {
|
||||
item.dialog === 'message'
|
||||
? openMessageDialog()
|
||||
: item.dialog === 'words'
|
||||
? openDialog(item.dialogRef)
|
||||
: item.dialog === 'cache'
|
||||
? openDialog(item.dialogRef)
|
||||
: openDialog(item.dialogRef)
|
||||
? openDialog(item.dialogRef)
|
||||
: item.dialog === 'cache'
|
||||
? openDialog(item.dialogRef)
|
||||
: openDialog(item.dialogRef)
|
||||
"
|
||||
>
|
||||
<VAvatar variant="text" size="48" rounded="lg">
|
||||
@@ -420,6 +431,29 @@ onMounted(() => {
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 定时服务弹窗 -->
|
||||
<VDialog
|
||||
v-if="schedulerDialog"
|
||||
v-model="schedulerDialog"
|
||||
max-width="60rem"
|
||||
scrollable
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<VCardTitle>
|
||||
<VIcon icon="mdi-list-box" class="me-2" />
|
||||
{{ t('shortcut.scheduler.subtitle') }}
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>{{ t('setting.scheduler.subtitle') }}</VCardSubtitle>
|
||||
<VDialogCloseBtn @click="schedulerDialog = false" />
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText class="pa-0">
|
||||
<AccountSettingService />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 系统健康检查弹窗 -->
|
||||
<VDialog
|
||||
v-if="systemTestDialog"
|
||||
|
||||
@@ -46,6 +46,7 @@ export default {
|
||||
unsubscribe: 'Unsubscribe',
|
||||
media: 'Media',
|
||||
unknown: 'Unknown',
|
||||
notFetched: 'Not Fetched',
|
||||
notice: 'Notice',
|
||||
itemsPerPage: 'Items per page',
|
||||
pageText: '{0}-{1} of {2}',
|
||||
@@ -314,7 +315,8 @@ export default {
|
||||
settingTabs: {
|
||||
system: {
|
||||
title: 'System',
|
||||
description: 'Basic settings, downloaders (Qbittorrent, Transmission), media servers (Emby, Jellyfin, Plex)',
|
||||
description:
|
||||
'Basic settings, downloaders (Qbittorrent, Transmission), media servers (Emby, Jellyfin, Plex, TrimeMedia, Ugreen)',
|
||||
},
|
||||
directory: {
|
||||
title: 'Storage & Directories',
|
||||
@@ -433,6 +435,8 @@ export default {
|
||||
config: 'Configuration',
|
||||
wechat: {
|
||||
name: 'WeChat Work',
|
||||
useBotMode: 'Use AI Bot',
|
||||
useBotModeHint: 'Enable WebSocket bot mode with fixed dmPolicy=open and groupPolicy=disabled',
|
||||
corpId: 'Corp ID',
|
||||
corpIdHint: 'Corp ID in WeChat Work backend enterprise information',
|
||||
corpIdRequired: 'Corp ID cannot be empty',
|
||||
@@ -449,6 +453,15 @@ export default {
|
||||
tokenHint: 'Token in WeChat Work self-built app -> API message receiving configuration',
|
||||
encodingAesKey: 'EncodingAESKey',
|
||||
encodingAesKeyHint: 'EncodingAESKey in WeChat Work self-built app -> API message receiving configuration',
|
||||
botId: 'Bot ID',
|
||||
botIdHint: 'Bot ID of the WeChat Work AI bot',
|
||||
botSecret: 'Bot Secret',
|
||||
botSecretHint: 'WebSocket secret of the WeChat Work AI bot',
|
||||
botChatId: 'Default Target',
|
||||
botChatIdHint: 'Use user userid; for proactive group messages use group:chatid. Leave empty to notify known interacted users',
|
||||
botChatIdPlaceholder: 'userid or group:chatid',
|
||||
botWsUrl: 'WebSocket URL',
|
||||
botWsUrlHint: 'WebSocket endpoint for the WeChat Work AI bot, usually the default value',
|
||||
admins: 'Admin Whitelist',
|
||||
adminsHint: 'User IDs that can use admin menu and commands, separated by commas',
|
||||
adminsPlaceholder: 'User IDs list, separated by commas',
|
||||
@@ -519,6 +532,21 @@ export default {
|
||||
usernameHint: 'Only push messages to the corresponding logged-in user',
|
||||
usernameRequired: 'Username cannot be empty',
|
||||
},
|
||||
qqbot: {
|
||||
name: 'QQ',
|
||||
appId: 'App ID',
|
||||
appIdHint: 'QQ Open Platform bot App ID',
|
||||
appIdRequired: 'App ID cannot be empty',
|
||||
appSecret: 'App Secret',
|
||||
appSecretHint: 'QQ Open Platform bot App Secret',
|
||||
appSecretRequired: 'App Secret cannot be empty',
|
||||
openId: 'User OpenID',
|
||||
openIdHint: 'Default recipient openid (C2C), user must have interacted with bot before',
|
||||
openIdPlaceholder: '32-char hex',
|
||||
groupOpenId: 'Group OpenID',
|
||||
groupOpenIdHint: 'Default group openid (group chat), use either this or User OpenID',
|
||||
groupOpenIdPlaceholder: 'Group openid',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
title: 'Shortcuts',
|
||||
@@ -554,6 +582,10 @@ export default {
|
||||
title: 'Cache',
|
||||
subtitle: 'Manage Cache',
|
||||
},
|
||||
scheduler: {
|
||||
title: 'Services',
|
||||
subtitle: 'Scheduled Services',
|
||||
},
|
||||
},
|
||||
workflow: {
|
||||
components: 'Action Components',
|
||||
@@ -1292,23 +1324,42 @@ export default {
|
||||
llmProviderHint: 'Select the LLM service provider to use',
|
||||
llmModel: 'LLM Model Name',
|
||||
llmModelHint: 'Specify the LLM model to use, such as gpt-3.5-turbo, deepseek-chat, etc.',
|
||||
llmMaxContextTokens: 'LLM Max Context Tokens (K)',
|
||||
llmMaxContextTokensHint:
|
||||
'Set the maximum number of context tokens (in thousands) for the LLM. Exceeding this limit will trigger context trimming.',
|
||||
llmApiKey: 'LLM API Key',
|
||||
llmApiKeyHint: 'API key from the LLM service provider for authentication',
|
||||
llmApiKeyPlaceholder: 'Please enter API key',
|
||||
llmBaseUrl: 'LLM Base URL',
|
||||
llmBaseUrlHint: 'Base URL for LLM API, used for custom API endpoints',
|
||||
aiAgentGlobal: 'Global AI Assistant',
|
||||
aiAgentGlobalHint: 'Enable global AI assistant functionality, all message conversations will be answered by the AI agent without using the /ai command',
|
||||
aiAgentGlobalHint:
|
||||
'Enable global AI assistant functionality, all message conversations will be answered by the AI agent without using the /ai command',
|
||||
aiAgentJobInterval: 'Scheduled Wake',
|
||||
aiAgentJobIntervalHint:
|
||||
'Set the check interval for scheduled wake. Select "Disabled" to disable scheduled tasks.',
|
||||
aiAgentVerbose: 'Verbose Mode',
|
||||
aiAgentVerboseHint: 'When enabled, tool call process will be displayed in AI agent responses',
|
||||
aiAgentJobIntervalDisabled: 'Disabled',
|
||||
aiAgentJobInterval1h: '1 Hour',
|
||||
aiAgentJobInterval3h: '3 Hours',
|
||||
aiAgentJobInterval6h: '6 Hours',
|
||||
aiAgentJobInterval12h: '12 Hours',
|
||||
aiAgentJobInterval24h: '24 Hours',
|
||||
aiAgentJobInterval1w: '1 Week',
|
||||
aiAgentJobInterval1M: '1 Month',
|
||||
advancedSettings: 'Advanced Settings',
|
||||
advancedSettingsDesc: 'System advanced settings, only need to be adjusted in special cases',
|
||||
downloaders: 'Downloaders',
|
||||
downloadersDesc: 'Only the default downloader will be used by default.',
|
||||
aiRecommendEnabled: 'AI Search Recommendation',
|
||||
aiRecommendEnabledHint: 'Enable AI search recommendation. When enabled, an AI recommendation button will be displayed on the search result page, recommending resources based on user preferences.',
|
||||
aiRecommendEnabledHint:
|
||||
'Enable AI search recommendation. When enabled, an AI recommendation button will be displayed on the search result page, recommending resources based on user preferences.',
|
||||
aiRecommendUserPreference: 'User Preference',
|
||||
aiRecommendUserPreferenceHint: 'Set user preferences for AI recommendation, e.g., 4K WEB-DL Dolby Vision',
|
||||
aiRecommendMaxItems: 'AI Recommendation Analysis Limit',
|
||||
aiRecommendMaxItemsHint: 'Limit the number of search results sent to the AI assistant for analysis. More items mean slower analysis and more token consumption. It is recommended to manually filter to a general range before using AI recommendation.',
|
||||
aiRecommendMaxItemsHint:
|
||||
'Limit the number of search results sent to the AI assistant for analysis. More items mean slower analysis and more token consumption. It is recommended to manually filter to a general range before using AI recommendation.',
|
||||
mediaServers: 'Media Servers',
|
||||
mediaServersDesc: 'All enabled media servers will be used.',
|
||||
trimeMedia: 'TrimeMedia',
|
||||
@@ -1331,9 +1382,11 @@ export default {
|
||||
reloading: 'Applying configuration...',
|
||||
qbittorrent: 'Qbittorrent',
|
||||
transmission: 'Transmission',
|
||||
rtorrent: 'rTorrent',
|
||||
emby: 'Emby',
|
||||
jellyfin: 'Jellyfin',
|
||||
plex: 'Plex',
|
||||
ugreen: 'Ugreen',
|
||||
reloadSuccess: 'System configuration has taken effect',
|
||||
reloadFailed: 'Failed to reload system!',
|
||||
auxAuthEnable: 'User Auxiliary Authentication',
|
||||
@@ -1376,6 +1429,8 @@ export default {
|
||||
fanartEnableHint: 'Use image data from fanart.tv',
|
||||
fanartLang: 'Fanart Language',
|
||||
fanartLangHint: 'Set language preference for Fanart images, ordered by priority when multiple selected',
|
||||
recognizePluginFirst: "Prioritize Plugin Recognition",
|
||||
recognizePluginFirstHint: "Prioritize calling plugins for media recognition. If a plugin matches, native recognition will be skipped",
|
||||
githubProxy: 'Github Acceleration Proxy',
|
||||
githubProxyPlaceholder: 'Leave empty for no proxy',
|
||||
githubProxyHint: 'Use proxy to accelerate Github access speed',
|
||||
@@ -1490,6 +1545,11 @@ export default {
|
||||
episodeThumb: 'Thumb',
|
||||
scrapingSwitchSaveFailed: 'Scraping switch settings save failed: {message}',
|
||||
scrapingSwitchSaveError: 'Scraping switch settings save failed',
|
||||
policy: {
|
||||
skipDesc: 'Skip scraping, this file will not be generated',
|
||||
missingOnlyDesc: 'Scrape only if missing, existing file remains unchanged',
|
||||
overwriteDesc: 'Always scrape, existing file will be overwritten',
|
||||
}
|
||||
},
|
||||
site: {
|
||||
siteSync: 'Site Synchronization',
|
||||
@@ -1587,6 +1647,7 @@ export default {
|
||||
synologyChat: 'SynologyChat',
|
||||
voceChat: 'VoceChat',
|
||||
webPush: 'WebPush',
|
||||
qq: 'QQ',
|
||||
custom: 'Custom Notification',
|
||||
},
|
||||
words: {
|
||||
@@ -1694,6 +1755,25 @@ export default {
|
||||
storageSaveSuccess: 'Storage settings saved successfully',
|
||||
storageSaveFailed: 'Failed to save storage settings!',
|
||||
},
|
||||
category: {
|
||||
title: 'Category Policy',
|
||||
subtitle: 'Configure media auto-categorization rules by type, language, region, etc.',
|
||||
movie: 'Movies',
|
||||
tv: 'TV Shows',
|
||||
name: 'Category Name (Directory)',
|
||||
genre: 'Genre',
|
||||
language: 'Language',
|
||||
languagePlaceholder: 'e.g., en,fr,zh (comma separated)',
|
||||
country: 'Country/Region',
|
||||
countryPlaceholder: 'e.g., US,CN,JP',
|
||||
year: 'Year',
|
||||
yearPlaceholder: 'e.g., 2023, 2020-2024',
|
||||
addMovie: 'Add Movie Category',
|
||||
addTv: 'Add TV Category',
|
||||
saveSuccess: 'Category policy saved successfully',
|
||||
loadFailed: 'Failed to load category configuration',
|
||||
saveFailed: 'Save failed: {message}',
|
||||
},
|
||||
rule: {
|
||||
customRules: 'Custom Rules',
|
||||
customRulesDesc: 'Custom priority rule items',
|
||||
@@ -1999,9 +2079,15 @@ export default {
|
||||
'Before sharing, please ensure the workflow does not contain sensitive information such as PassKey in RSS links to avoid information leakage.',
|
||||
},
|
||||
u115Auth: {
|
||||
loginTitle: '115 Cloud Login',
|
||||
scanQrCode: 'Please scan with WeChat or 115 client',
|
||||
scanned: 'Scanned, please confirm login',
|
||||
loginTitle: '115 Cloud Authorization',
|
||||
openAuthWindow: 'Open Authorization Window',
|
||||
authorizing: 'Please complete authorization in the new window...',
|
||||
authSuccess: 'Authorization successful!',
|
||||
authFailed: 'Authorization failed or expired',
|
||||
authCanceled: 'Authorization canceled, please try again',
|
||||
urlEmpty: 'Authorization URL is empty',
|
||||
urlFetchFailed: 'Failed to fetch authorization URL',
|
||||
popupBlocked: 'Unable to open authorization window, please check browser popup settings',
|
||||
complete: 'Complete',
|
||||
reset: 'Reset',
|
||||
},
|
||||
@@ -2617,7 +2703,8 @@ export default {
|
||||
passkeyManagement: 'Passkey Management',
|
||||
registerNewPasskey: 'Register New Passkey',
|
||||
passkeyDescription: 'Passkeys allow you to sign in quickly and securely without a password.',
|
||||
passkeyAppDescription: 'Passkeys are a simpler, more secure way to sign in, serving as an alternative to passwords. You can authenticate using passkey-supported apps like iCloud Keychain, Bitwarden, or hardware keys.',
|
||||
passkeyAppDescription:
|
||||
'Passkeys are a simpler, more secure way to sign in, serving as an alternative to passwords. You can authenticate using passkey-supported apps like iCloud Keychain, Bitwarden, or hardware keys.',
|
||||
passkeyName: 'Passkey Name',
|
||||
passkeyNamePlaceholder: 'e.g.: iPhone, Windows Hello',
|
||||
registerPasskey: 'Register Passkey',
|
||||
@@ -2631,8 +2718,10 @@ export default {
|
||||
passkeyDeleteSuccess: 'Passkey deleted',
|
||||
passkeyDeleteFailed: 'Delete failed',
|
||||
deletePasskey: 'Delete Passkey',
|
||||
passkeyDomainWarning: 'The availability of PassKeys is closely related to the {domain}. In a public network environment, please make sure to configure the correct access domain name in "Basic Settings". Domain changes or configuration errors will cause the PassKey to be unusable.',
|
||||
otpRequiredForPasskey: 'For security reasons, you must first enable {otp} before you can register a PassKey. This is to ensure that you can still log in to your account via OTP code if the PassKey becomes invalid due to domain configuration changes.',
|
||||
passkeyDomainWarning:
|
||||
'The availability of PassKeys is closely related to the {domain}. In a public network environment, please make sure to configure the correct access domain name in "Basic Settings". Domain changes or configuration errors will cause the PassKey to be unusable.',
|
||||
otpRequiredForPasskey:
|
||||
'For security reasons, you must first enable {otp} before you can register a PassKey. This is to ensure that you can still log in to your account via OTP code if the PassKey becomes invalid due to domain configuration changes.',
|
||||
accessDomain: 'access domain name',
|
||||
otpAuthenticator: 'OTP Authenticator',
|
||||
otpGenerateFailed: 'Failed to get OTP URI: {message}!',
|
||||
@@ -2641,8 +2730,10 @@ export default {
|
||||
otpCodeRequired: 'Please enter the 6-digit verification code',
|
||||
otpEnableSuccess: 'Two-factor authentication enabled successfully!',
|
||||
otpEnableFailed: 'Failed to enable OTP: {message}!',
|
||||
otpDisableRestrictedByPasskey: 'You have registered Passkeys. Please delete all Passkeys before disabling OTP verification.',
|
||||
confirmToDisableOtp: 'For security reasons, verifying your login password is required to disable two-factor authentication.',
|
||||
otpDisableRestrictedByPasskey:
|
||||
'You have registered Passkeys. Please delete all Passkeys before disabling OTP verification.',
|
||||
confirmToDisableOtp:
|
||||
'For security reasons, verifying your login password is required to disable two-factor authentication.',
|
||||
confirmToDeletePasskey: 'For security reasons, verifying your login password is required to delete a Passkey.',
|
||||
authenticatorAppDescription:
|
||||
'Use an authenticator app like Google Authenticator, Microsoft Authenticator, Authy, or 1Password to scan the QR code and generate a 6-digit code.',
|
||||
@@ -2746,6 +2837,7 @@ export default {
|
||||
type: 'Type',
|
||||
enabled: 'Enabled',
|
||||
customTypeHint: 'Custom downloader type, for plugin scenarios',
|
||||
rtorrentHostHint: 'HTTP: http://ip:port/RPC2 or SCGI: scgi://ip:port',
|
||||
default: 'Default',
|
||||
host: 'Host',
|
||||
username: 'Username',
|
||||
@@ -2815,6 +2907,15 @@ export default {
|
||||
password: 'Password',
|
||||
syncLibraries: 'Sync Libraries',
|
||||
syncLibrariesHint: 'Only selected libraries will be synchronized',
|
||||
scanMode: 'Scan Mode',
|
||||
scanModeHint: 'Applies to full-library and targeted refresh: New & Modified / Supplement Missing / Full Override',
|
||||
verifySsl: 'Verify SSL Certificate',
|
||||
verifySslHint: 'When enabled, HTTPS certificates are verified; disable for self-signed certificates',
|
||||
scanModeOptions: {
|
||||
newAndModified: 'New & Modified',
|
||||
supplementMissing: 'Supplement Missing',
|
||||
fullOverride: 'Full Override',
|
||||
},
|
||||
hostRequired: 'Host cannot be empty',
|
||||
apiKeyRequired: 'API Key cannot be empty',
|
||||
tokenRequired: 'Token cannot be empty',
|
||||
@@ -3122,7 +3223,8 @@ export default {
|
||||
title: 'Media Server',
|
||||
description: 'Configure media server',
|
||||
info: 'Media Server Configuration',
|
||||
infoDesc: 'Configure media server for media library management, can choose Emby, Jellyfin or Plex etc.',
|
||||
infoDesc:
|
||||
'Configure media server for media library management, can choose Emby, Jellyfin, Plex, TrimeMedia or Ugreen.',
|
||||
type: 'Media Server Type',
|
||||
typeHint: 'Select the type of media server to use',
|
||||
name: 'Server Name',
|
||||
|
||||
@@ -46,6 +46,7 @@ export default {
|
||||
unsubscribe: '取消订阅',
|
||||
media: '媒体',
|
||||
unknown: '未知',
|
||||
notFetched: '未获取',
|
||||
notice: '注意',
|
||||
itemsPerPage: '每页条数',
|
||||
pageText: '{0}-{1} 共 {2} 条',
|
||||
@@ -313,7 +314,7 @@ export default {
|
||||
settingTabs: {
|
||||
system: {
|
||||
title: '系统',
|
||||
description: '基础设置、下载器(Qbittorrent、Transmission)、媒体服务器(Emby、Jellyfin、Plex)',
|
||||
description: '基础设置、下载器(Qbittorrent、Transmission)、媒体服务器(Emby、Jellyfin、Plex、飞牛影视、绿联影视)',
|
||||
},
|
||||
directory: {
|
||||
title: '存储 & 目录',
|
||||
@@ -432,6 +433,8 @@ export default {
|
||||
config: '配置',
|
||||
wechat: {
|
||||
name: '企业微信',
|
||||
useBotMode: '使用智能机器人',
|
||||
useBotModeHint: '开启后使用智能机器人长连接,固定 dmPolicy=open、groupPolicy=disabled',
|
||||
corpId: '企业ID',
|
||||
corpIdHint: '企业微信后台企业信息中的企业ID',
|
||||
corpIdRequired: '企业ID不能为空',
|
||||
@@ -447,6 +450,15 @@ export default {
|
||||
tokenHint: '微信企业自建应用->API接收消息配置中的Token',
|
||||
encodingAesKey: 'EncodingAESKey',
|
||||
encodingAesKeyHint: '微信企业自建应用->API接收消息配置中的EncodingAESKey',
|
||||
botId: '机器人 BotID',
|
||||
botIdHint: '企业微信智能机器人的 BotID',
|
||||
botSecret: '机器人 Secret',
|
||||
botSecretHint: '企业微信智能机器人长连接专用 Secret',
|
||||
botChatId: '默认通知目标',
|
||||
botChatIdHint: '可填写用户 userid;如需主动发群消息可填写 group:群聊chatid,不填则默认发给已互动用户',
|
||||
botChatIdPlaceholder: 'userid 或 group:chatid',
|
||||
botWsUrl: '长连接地址',
|
||||
botWsUrlHint: '企业微信智能机器人 WebSocket 地址,通常使用默认值',
|
||||
admins: '管理员白名单',
|
||||
adminsHint: '可使用管理菜单及命令的用户ID列表,多个ID使用,分隔',
|
||||
adminsPlaceholder: '用户ID列表,多个ID使用,分隔',
|
||||
@@ -517,6 +529,21 @@ export default {
|
||||
usernameHint: '只有对应的用户登录后才会推送消息',
|
||||
usernameRequired: '用户名不能为空',
|
||||
},
|
||||
qqbot: {
|
||||
name: 'QQ',
|
||||
appId: 'AppID',
|
||||
appIdHint: 'QQ 开放平台机器人 AppID',
|
||||
appIdRequired: 'AppID 不能为空',
|
||||
appSecret: 'AppSecret',
|
||||
appSecretHint: 'QQ 开放平台机器人 AppSecret',
|
||||
appSecretRequired: 'AppSecret 不能为空',
|
||||
openId: '用户 OpenID',
|
||||
openIdHint: '默认接收者 openid(单聊),用户需曾与机器人交互过',
|
||||
openIdPlaceholder: '32位十六进制',
|
||||
groupOpenId: '群组 OpenID',
|
||||
groupOpenIdHint: '默认群组 openid(群聊),与用户 OpenID 二选一',
|
||||
groupOpenIdPlaceholder: '群组 openid',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
title: '捷径',
|
||||
@@ -552,6 +579,10 @@ export default {
|
||||
title: '缓存',
|
||||
subtitle: '管理缓存',
|
||||
},
|
||||
scheduler: {
|
||||
title: '服务',
|
||||
subtitle: '定时服务',
|
||||
},
|
||||
},
|
||||
workflow: {
|
||||
components: '动作组件',
|
||||
@@ -1288,6 +1319,9 @@ export default {
|
||||
llmProviderHint: '选择使用的LLM服务提供商',
|
||||
llmModel: 'LLM模型名称',
|
||||
llmModelHint: '指定使用的LLM模型,如gpt-3.5-turbo、deepseek-chat等',
|
||||
llmMaxContextTokens: 'LLM 最大上下文 Token 数量 (K)',
|
||||
llmMaxContextTokensHint:
|
||||
'设定 LLM 记录会话历史的最大 Token 数量上限(千),超出后将自动修整历史记录以节省 Token 消耗及防止超出 LLM 限制',
|
||||
llmApiKey: 'LLM API密钥',
|
||||
llmApiKeyHint: 'LLM服务提供商的API密钥,用于身份验证',
|
||||
llmApiKeyPlaceholder: '请输入API密钥',
|
||||
@@ -1295,16 +1329,30 @@ export default {
|
||||
llmBaseUrlHint: 'LLM API的基础URL地址,用于自定义API端点',
|
||||
aiAgentGlobal: '全局智能助手',
|
||||
aiAgentGlobalHint: '启用全局智能助手功能,所有消息对话均使用智能体回答而不用使用/ai命令',
|
||||
aiAgentJobInterval: '定时唤醒',
|
||||
aiAgentJobIntervalHint: '设置定时唤醒的检查间隔,选择"不启用"则不执行定时任务',
|
||||
aiAgentVerbose: '啰嗦模式',
|
||||
aiAgentVerboseHint: '开启后会在智能体回复时显示工具调用过程',
|
||||
aiAgentJobIntervalDisabled: '不启用',
|
||||
aiAgentJobInterval1h: '1小时',
|
||||
aiAgentJobInterval3h: '3小时',
|
||||
aiAgentJobInterval6h: '6小时',
|
||||
aiAgentJobInterval12h: '12小时',
|
||||
aiAgentJobInterval24h: '24小时',
|
||||
aiAgentJobInterval1w: '1周',
|
||||
aiAgentJobInterval1M: '1个月',
|
||||
advancedSettings: '高级设置',
|
||||
advancedSettingsDesc: '系统进阶设置,特殊情况下才需要调整',
|
||||
downloaders: '下载器',
|
||||
downloadersDesc: '只有默认下载器才会被默认使用。',
|
||||
aiRecommendEnabled: '搜索结果智能推荐',
|
||||
aiRecommendEnabledHint: '启用搜索结果智能推荐功能,开启后将在搜索结果页面显示智能推荐按钮,可根据用户偏好智能推荐资源',
|
||||
aiRecommendEnabledHint:
|
||||
'启用搜索结果智能推荐功能,开启后将在搜索结果页面显示智能推荐按钮,可根据用户偏好智能推荐资源',
|
||||
aiRecommendUserPreference: '用户偏好',
|
||||
aiRecommendUserPreferenceHint: '设置智能推荐时的用户偏好,例如:4K WEB-DL Dolby Vision',
|
||||
aiRecommendMaxItems: '智能推荐分析条目上限',
|
||||
aiRecommendMaxItemsHint: '限制发送给智能助手进行分析的搜索结果数量,数量越多分析越慢且消耗 Token 越多,建议先手动筛选,筛选出大致范围后再进行智能推荐',
|
||||
aiRecommendMaxItemsHint:
|
||||
'限制发送给智能助手进行分析的搜索结果数量,数量越多分析越慢且消耗 Token 越多,建议先手动筛选,筛选出大致范围后再进行智能推荐',
|
||||
mediaServers: '媒体服务器',
|
||||
mediaServersDesc: '所有启用的媒体服务器都会被使用。',
|
||||
trimeMedia: '飞牛影视',
|
||||
@@ -1327,9 +1375,11 @@ export default {
|
||||
reloading: '正在应用配置...',
|
||||
qbittorrent: 'Qbittorrent',
|
||||
transmission: 'Transmission',
|
||||
rtorrent: 'rTorrent',
|
||||
emby: 'Emby',
|
||||
jellyfin: 'Jellyfin',
|
||||
plex: 'Plex',
|
||||
ugreen: '绿联影视',
|
||||
reloadSuccess: '系统配置已生效',
|
||||
reloadFailed: '重载系统失败!',
|
||||
auxAuthEnable: '用户辅助认证',
|
||||
@@ -1369,6 +1419,8 @@ export default {
|
||||
fanartEnableHint: '使用 fanart.tv 的图片数据',
|
||||
fanartLang: 'Fanart语言',
|
||||
fanartLangHint: '设置Fanart图片的语言偏好,多选时按优先级顺序排列',
|
||||
recognizePluginFirst: "优先使用插件识别",
|
||||
recognizePluginFirstHint: "优先调用插件识别媒体信息,若插件命中则不再调用原生识别",
|
||||
githubProxy: 'Github加速代理',
|
||||
githubProxyPlaceholder: '留空表示不使用代理',
|
||||
githubProxyHint: '使用代理加速Github访问速度',
|
||||
@@ -1479,6 +1531,11 @@ export default {
|
||||
episodeThumb: '缩略图',
|
||||
scrapingSwitchSaveFailed: '刮削开关设置保存失败:{message}',
|
||||
scrapingSwitchSaveError: '刮削开关设置保存失败',
|
||||
policy: {
|
||||
skipDesc: '跳过刮削,不生成该文件',
|
||||
missingOnlyDesc: '仅在缺失时刮削,已存在则保持不变',
|
||||
overwriteDesc: '始终刮削,已存在则覆盖',
|
||||
}
|
||||
},
|
||||
site: {
|
||||
siteSync: '站点同步',
|
||||
@@ -1573,6 +1630,7 @@ export default {
|
||||
synologyChat: 'SynologyChat',
|
||||
voceChat: 'VoceChat',
|
||||
webPush: 'WebPush',
|
||||
qq: 'QQ',
|
||||
custom: '自定义通知',
|
||||
},
|
||||
words: {
|
||||
@@ -1672,6 +1730,25 @@ export default {
|
||||
storageSaveSuccess: '存储设置保存成功',
|
||||
storageSaveFailed: '存储设置保存失败!',
|
||||
},
|
||||
category: {
|
||||
title: '分类策略',
|
||||
subtitle: '配置媒体自动分类规则,按类型、语言、地区等条件自动归类',
|
||||
movie: '电影 (Movie)',
|
||||
tv: '电视剧 (TV)',
|
||||
name: '分类名称 (目录名)',
|
||||
genre: '内容类型 (Genre)',
|
||||
language: '语种 (Language)',
|
||||
languagePlaceholder: '如: zh,cn,en (使用逗号分隔)',
|
||||
country: '国家/地区 (Country)',
|
||||
countryPlaceholder: '如: US,CN,JP',
|
||||
year: '年份 (Year)',
|
||||
yearPlaceholder: '如: 2023, 2020-2024',
|
||||
addMovie: '添加电影分类',
|
||||
addTv: '添加电视剧分类',
|
||||
saveSuccess: '分类策略保存成功',
|
||||
loadFailed: '加载分类配置失败',
|
||||
saveFailed: '保存失败: {message}',
|
||||
},
|
||||
rule: {
|
||||
customRules: '自定义规则',
|
||||
customRulesDesc: '自定义优先级规则项',
|
||||
@@ -1971,9 +2048,15 @@ export default {
|
||||
securityWarningMessage: '分享前请确保工作流没有敏感信息,比如RSS链接中的PassKey等,避免产生信息泄露。',
|
||||
},
|
||||
u115Auth: {
|
||||
loginTitle: '115网盘登录',
|
||||
scanQrCode: '请使用微信或115客户端扫码',
|
||||
scanned: '已扫码,请确认登录',
|
||||
loginTitle: '115网盘授权',
|
||||
openAuthWindow: '打开授权窗口',
|
||||
authorizing: '请在新窗口中完成授权...',
|
||||
authSuccess: '授权成功!',
|
||||
authFailed: '授权失败或已过期',
|
||||
authCanceled: '授权已取消,请重试',
|
||||
urlEmpty: '授权URL为空',
|
||||
urlFetchFailed: '获取授权URL失败',
|
||||
popupBlocked: '无法打开授权窗口,请检查浏览器弹窗设置',
|
||||
complete: '完成',
|
||||
reset: '重置',
|
||||
},
|
||||
@@ -2585,7 +2668,8 @@ export default {
|
||||
passkeyManagement: '通行密钥管理',
|
||||
registerNewPasskey: '注册新通行密钥',
|
||||
passkeyDescription: '通行密钥可以让您无需密码即可快速安全地登录。',
|
||||
passkeyAppDescription: '通行密钥是一种更简单、更安全的登录方式,可以替代密码进行登录。您可以使用 iCloud 钥匙串、Bitwarden 等支持通行密钥的应用程序或硬件密钥完成验证。',
|
||||
passkeyAppDescription:
|
||||
'通行密钥是一种更简单、更安全的登录方式,可以替代密码进行登录。您可以使用 iCloud 钥匙串、Bitwarden 等支持通行密钥的应用程序或硬件密钥完成验证。',
|
||||
passkeyName: '通行密钥名称',
|
||||
passkeyNamePlaceholder: '例如:iPhone、Windows Hello',
|
||||
registerPasskey: '注册通行密钥',
|
||||
@@ -2599,8 +2683,10 @@ export default {
|
||||
passkeyDeleteSuccess: '通行密钥已删除',
|
||||
passkeyDeleteFailed: '删除失败',
|
||||
deletePasskey: '删除通行密钥',
|
||||
passkeyDomainWarning: '通行密钥(PassKey)的可用性与 {domain} 紧密相关。在公网环境下,请务必在“基础设置”中配置正确的访问域名。域名变更或配置错误将导致通行密钥无法使用。',
|
||||
otpRequiredForPasskey: '为了安全起见,您必须先启用 {otp} 验证码,然后才能注册通行密钥。这是为了防止在域名配置变动导致 PassKey 失效时,您仍能通过 OTP 码登录账户。',
|
||||
passkeyDomainWarning:
|
||||
'通行密钥(PassKey)的可用性与 {domain} 紧密相关。在公网环境下,请务必在“基础设置”中配置正确的访问域名。域名变更或配置错误将导致通行密钥无法使用。',
|
||||
otpRequiredForPasskey:
|
||||
'为了安全起见,您必须先启用 {otp} 验证码,然后才能注册通行密钥。这是为了防止在域名配置变动导致 PassKey 失效时,您仍能通过 OTP 码登录账户。',
|
||||
accessDomain: '访问域名',
|
||||
otpAuthenticator: 'OTP 身份验证器',
|
||||
otpGenerateFailed: '获取otp uri失败:{message}!',
|
||||
@@ -2713,6 +2799,7 @@ export default {
|
||||
type: '类型',
|
||||
enabled: '启用',
|
||||
customTypeHint: '自定义下载器类型,用于插件等场景',
|
||||
rtorrentHostHint: 'HTTP: http://ip:port/RPC2 或 SCGI: scgi://ip:port',
|
||||
default: '默认',
|
||||
host: '地址',
|
||||
username: '用户名',
|
||||
@@ -2782,6 +2869,15 @@ export default {
|
||||
password: '密码',
|
||||
syncLibraries: '同步媒体库',
|
||||
syncLibrariesHint: '只有选中的媒体库才会被同步',
|
||||
scanMode: '扫描模式',
|
||||
scanModeHint: '用于全库刷新和按库刷新:新添加和修改 / 补充缺失 / 覆盖扫描',
|
||||
verifySsl: '校验 SSL 证书',
|
||||
verifySslHint: '开启后会校验 HTTPS 证书;如使用自签名证书可关闭',
|
||||
scanModeOptions: {
|
||||
newAndModified: '新添加和修改',
|
||||
supplementMissing: '补充缺失',
|
||||
fullOverride: '覆盖扫描',
|
||||
},
|
||||
nameExists: '【{name}】已存在,请替换为其他名称',
|
||||
hostRequired: '地址不能为空',
|
||||
apiKeyRequired: 'API密钥不能为空',
|
||||
@@ -3088,7 +3184,7 @@ export default {
|
||||
title: '媒体服务器',
|
||||
description: '配置媒体服务器',
|
||||
info: '媒体服务器配置说明',
|
||||
infoDesc: '配置媒体服务器用于媒体库管理,可选择Emby、Jellyfin或Plex等',
|
||||
infoDesc: '配置媒体服务器用于媒体库管理,可选择Emby、Jellyfin、Plex、飞牛影视或绿联影视',
|
||||
type: '媒体服务器类型',
|
||||
typeHint: '选择要使用的媒体服务器类型',
|
||||
name: '服务器名称',
|
||||
@@ -3161,3 +3257,7 @@ export default {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Apply patch to add category strings
|
||||
// This is a temporary placeholder command to show intent.
|
||||
// I will use replace_file_content to actually edit the file safely.
|
||||
|
||||
@@ -46,6 +46,7 @@ export default {
|
||||
unsubscribe: '取消訂閱',
|
||||
media: '媒體',
|
||||
unknown: '未知',
|
||||
notFetched: '未獲取',
|
||||
notice: '注意',
|
||||
itemsPerPage: '每頁條數',
|
||||
pageText: '{0}-{1} 共 {2} 條',
|
||||
@@ -313,7 +314,7 @@ export default {
|
||||
settingTabs: {
|
||||
system: {
|
||||
title: '系統',
|
||||
description: '基礎設置、下載器(Qbittorrent、Transmission)、媒體服務器(Emby、Jellyfin、Plex)',
|
||||
description: '基礎設置、下載器(Qbittorrent、Transmission)、媒體服務器(Emby、Jellyfin、Plex、飛牛影視、綠聯影視)',
|
||||
},
|
||||
directory: {
|
||||
title: '存儲 & 目錄',
|
||||
@@ -432,6 +433,8 @@ export default {
|
||||
config: '配置',
|
||||
wechat: {
|
||||
name: '企業微信',
|
||||
useBotMode: '使用智能機器人',
|
||||
useBotModeHint: '開啟後使用智能機器人長連線,固定 dmPolicy=open、groupPolicy=disabled',
|
||||
corpId: '企業ID',
|
||||
corpIdHint: '企業微信後台企業信息中的企業ID',
|
||||
corpIdRequired: '企業ID不能為空',
|
||||
@@ -447,6 +450,15 @@ export default {
|
||||
tokenHint: '微信企業自建應用->API接收消息配置中的Token',
|
||||
encodingAesKey: 'EncodingAESKey',
|
||||
encodingAesKeyHint: '微信企業自建應用->API接收消息配置中的EncodingAESKey',
|
||||
botId: '機器人 BotID',
|
||||
botIdHint: '企業微信智能機器人的 BotID',
|
||||
botSecret: '機器人 Secret',
|
||||
botSecretHint: '企業微信智能機器人長連線專用 Secret',
|
||||
botChatId: '預設通知目標',
|
||||
botChatIdHint: '可填寫使用者 userid;如需主動發群消息可填寫 group:群聊chatid,不填則預設發給已互動使用者',
|
||||
botChatIdPlaceholder: 'userid 或 group:chatid',
|
||||
botWsUrl: '長連線地址',
|
||||
botWsUrlHint: '企業微信智能機器人 WebSocket 位址,通常使用預設值',
|
||||
admins: '管理員白名單',
|
||||
adminsHint: '可使用管理菜單及命令的用戶ID列表,多個ID使用,分隔',
|
||||
adminsPlaceholder: '用戶ID列表,多個ID使用,分隔',
|
||||
@@ -517,6 +529,21 @@ export default {
|
||||
usernameHint: '只有對應的用戶登錄後才會推送消息',
|
||||
usernameRequired: '用戶名不能為空',
|
||||
},
|
||||
qqbot: {
|
||||
name: 'QQ',
|
||||
appId: 'AppID',
|
||||
appIdHint: 'QQ 開放平台機器人 AppID',
|
||||
appIdRequired: 'AppID 不能為空',
|
||||
appSecret: 'AppSecret',
|
||||
appSecretHint: 'QQ 開放平台機器人 AppSecret',
|
||||
appSecretRequired: 'AppSecret 不能為空',
|
||||
openId: '用戶 OpenID',
|
||||
openIdHint: '默認接收者 openid(單聊),用戶需曾與機器人交互過',
|
||||
openIdPlaceholder: '32位十六進制',
|
||||
groupOpenId: '群組 OpenID',
|
||||
groupOpenIdHint: '默認群組 openid(群聊),與用戶 OpenID 二選一',
|
||||
groupOpenIdPlaceholder: '群組 openid',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
title: '捷徑',
|
||||
@@ -552,6 +579,10 @@ export default {
|
||||
title: '緩存',
|
||||
subtitle: '管理緩存',
|
||||
},
|
||||
scheduler: {
|
||||
title: '服務',
|
||||
subtitle: '定時服務',
|
||||
},
|
||||
},
|
||||
workflow: {
|
||||
components: '動作組件',
|
||||
@@ -1289,6 +1320,9 @@ export default {
|
||||
llmProviderHint: '選擇使用的LLM服務提供商',
|
||||
llmModel: 'LLM模型名稱',
|
||||
llmModelHint: '指定使用的LLM模型,如gpt-3.5-turbo、deepseek-chat等',
|
||||
llmMaxContextTokens: 'LLM 最大上下文 Token 數量 (K)',
|
||||
llmMaxContextTokensHint:
|
||||
'設定 LLM 記錄會話歷史的最大 Token 數量上限(千),超出後將自動修整歷史記錄以節省 Token 消耗及防止超出 LLM 限制',
|
||||
llmApiKey: 'LLM API密鑰',
|
||||
llmApiKeyHint: 'LLM服務提供商的API密鑰,用於身份驗證',
|
||||
llmApiKeyPlaceholder: '請輸入API密鑰',
|
||||
@@ -1296,16 +1330,30 @@ export default {
|
||||
llmBaseUrlHint: 'LLM API的基礎URL地址,用於自定義API端點',
|
||||
aiAgentGlobal: '全局智能助手',
|
||||
aiAgentGlobalHint: '啟用全局智能助手功能,所有消息對話均使用智能體回答而不用使用/ai命令',
|
||||
aiAgentJobInterval: '定時喚醒',
|
||||
aiAgentJobIntervalHint: '設置定時喚醒的檢查間隔,選擇「不啟用」則不執行定時任務',
|
||||
aiAgentVerbose: '囉嗦模式',
|
||||
aiAgentVerboseHint: '開啟後會在智能體回覆時顯示工具調用過程',
|
||||
aiAgentJobIntervalDisabled: '不啟用',
|
||||
aiAgentJobInterval1h: '1小時',
|
||||
aiAgentJobInterval3h: '3小時',
|
||||
aiAgentJobInterval6h: '6小時',
|
||||
aiAgentJobInterval12h: '12小時',
|
||||
aiAgentJobInterval24h: '24小時',
|
||||
aiAgentJobInterval1w: '1週',
|
||||
aiAgentJobInterval1M: '1個月',
|
||||
advancedSettings: '高級設置',
|
||||
advancedSettingsDesc: '系統進階設置,特殊情況下才需要調整',
|
||||
downloaders: '下載器',
|
||||
downloadersDesc: '只有默認下載器才會被默認使用。',
|
||||
aiRecommendEnabled: '搜索結果智能推薦',
|
||||
aiRecommendEnabledHint: '啟用搜索結果智能推薦功能,開啟後將在搜索結果頁面顯示智能推薦按鈕,可根據用戶偏好智能推薦資源',
|
||||
aiRecommendEnabledHint:
|
||||
'啟用搜索結果智能推薦功能,開啟後將在搜索結果頁面顯示智能推薦按鈕,可根據用戶偏好智能推薦資源',
|
||||
aiRecommendUserPreference: '用戶偏好',
|
||||
aiRecommendUserPreferenceHint: '設置智能推薦時的用戶偏好,例如:4K WEB-DL Dolby Vision',
|
||||
aiRecommendMaxItems: '智能推薦分析條目上限',
|
||||
aiRecommendMaxItemsHint: '限制發送給智能助手進行分析的搜索結果數量,數量越多分析越慢且消耗 Token 越多,建議先手動篩選,篩選出大致範圍後再進行智能推薦',
|
||||
aiRecommendMaxItemsHint:
|
||||
'限制發送給智能助手進行分析的搜索結果數量,數量越多分析越慢且消耗 Token 越多,建議先手動篩選,篩選出大致範圍後再進行智能推薦',
|
||||
mediaServers: '媒體服務器',
|
||||
mediaServersDesc: '所有啟用的媒體服務器都會被使用。',
|
||||
trimeMedia: '飛牛影視',
|
||||
@@ -1328,9 +1376,11 @@ export default {
|
||||
reloading: '正在應用配置...',
|
||||
qbittorrent: 'Qbittorrent',
|
||||
transmission: 'Transmission',
|
||||
rtorrent: 'rTorrent',
|
||||
emby: 'Emby',
|
||||
jellyfin: 'Jellyfin',
|
||||
plex: 'Plex',
|
||||
ugreen: '綠聯影視',
|
||||
reloadSuccess: '系統配置已生效',
|
||||
reloadFailed: '重載系統失敗!',
|
||||
auxAuthEnable: '用戶輔助認證',
|
||||
@@ -1370,6 +1420,8 @@ export default {
|
||||
fanartEnableHint: '使用 fanart.tv 的圖片數據',
|
||||
fanartLang: 'Fanart語言',
|
||||
fanartLangHint: '設定Fanart圖片的語言偏好,多選時按優先級順序排列',
|
||||
recognizePluginFirst: "優先使用插件識別",
|
||||
recognizePluginFirstHint: "優先調用插件識別媒體信息,若插件命中則不再調用原生識別",
|
||||
githubProxy: 'Github加速代理',
|
||||
githubProxyPlaceholder: '留空表示不使用代理',
|
||||
githubProxyHint: '使用代理加速Github訪問速度',
|
||||
@@ -1480,6 +1532,11 @@ export default {
|
||||
episodeThumb: '縮略圖',
|
||||
scrapingSwitchSaveFailed: '刮削開關設定保存失敗:{message}',
|
||||
scrapingSwitchSaveError: '刮削開關設定保存失敗',
|
||||
policy: {
|
||||
skipDesc: '跳過刮削,不生成該文件',
|
||||
missingOnlyDesc: '僅在缺失時刮削,已存在則保持不變',
|
||||
overwriteDesc: '始終刮削,已存在則覆蓋',
|
||||
}
|
||||
},
|
||||
site: {
|
||||
siteSync: '站點同步',
|
||||
@@ -1574,6 +1631,7 @@ export default {
|
||||
synologyChat: 'SynologyChat',
|
||||
voceChat: 'VoceChat',
|
||||
webPush: 'WebPush',
|
||||
qq: 'QQ',
|
||||
custom: '自定義通知',
|
||||
},
|
||||
words: {
|
||||
@@ -1673,6 +1731,25 @@ export default {
|
||||
storageSaveSuccess: '存儲設置保存成功',
|
||||
storageSaveFailed: '存儲設置保存失敗!',
|
||||
},
|
||||
category: {
|
||||
title: '分類策略',
|
||||
subtitle: '配置媒體自動分類規則,按類型、語言、地區等條件自動歸類',
|
||||
movie: '電影 (Movie)',
|
||||
tv: '電視劇 (TV)',
|
||||
name: '分類名稱 (目錄名)',
|
||||
genre: '內容類型 (Genre)',
|
||||
language: '語種 (Language)',
|
||||
languagePlaceholder: '如: zh,cn,en (使用逗號分隔)',
|
||||
country: '國家/地區 (Country)',
|
||||
countryPlaceholder: '如: US,CN,JP',
|
||||
year: '年份 (Year)',
|
||||
yearPlaceholder: '如: 2023, 2020-2024',
|
||||
addMovie: '添加電影分類',
|
||||
addTv: '添加電視劇分類',
|
||||
saveSuccess: '分類策略保存成功',
|
||||
loadFailed: '加載分類配置失敗',
|
||||
saveFailed: '保存失敗: {message}',
|
||||
},
|
||||
rule: {
|
||||
customRules: '自定義規則',
|
||||
customRulesDesc: '自定義優先級規則項',
|
||||
@@ -1972,9 +2049,15 @@ export default {
|
||||
securityWarningMessage: '分享前請確保工作流沒有敏感資訊,比如RSS連結中的PassKey等,避免產生資訊洩露。',
|
||||
},
|
||||
u115Auth: {
|
||||
loginTitle: '115網盤登錄',
|
||||
scanQrCode: '請使用微信或115客戶端掃碼',
|
||||
scanned: '已掃碼,請確認登錄',
|
||||
loginTitle: '115網盤授權',
|
||||
openAuthWindow: '打開授權窗口',
|
||||
authorizing: '請在新窗口中完成授權...',
|
||||
authSuccess: '授權成功!',
|
||||
authFailed: '授權失敗或已過期',
|
||||
authCanceled: '授權已取消,請重試',
|
||||
urlEmpty: '授權URL為空',
|
||||
urlFetchFailed: '獲取授權URL失敗',
|
||||
popupBlocked: '無法打開授權窗口,請檢查瀏覽器彈窗設置',
|
||||
complete: '完成',
|
||||
reset: '重置',
|
||||
},
|
||||
@@ -2586,7 +2669,8 @@ export default {
|
||||
passkeyManagement: '通行密鑰管理',
|
||||
registerNewPasskey: '註冊新通行密鑰',
|
||||
passkeyDescription: '通行密鑰可以讓您無需密碼即可快速安全地登入。',
|
||||
passkeyAppDescription: '通行密鑰是一種更簡單、更安全的登入方式,可以替代密碼進行登入。您可以使用 iCloud 鑰匙圈、Bitwarden 等支援通行密鑰的應用程式或硬體金鑰完成驗證。',
|
||||
passkeyAppDescription:
|
||||
'通行密鑰是一種更簡單、更安全的登入方式,可以替代密碼進行登入。您可以使用 iCloud 鑰匙圈、Bitwarden 等支援通行密鑰的應用程式或硬體金鑰完成驗證。',
|
||||
passkeyName: '通行密鑰名稱',
|
||||
passkeyNamePlaceholder: '例如:iPhone、Windows Hello',
|
||||
registerPasskey: '註冊通行密鑰',
|
||||
@@ -2715,6 +2799,7 @@ export default {
|
||||
name: '名稱',
|
||||
type: '類型',
|
||||
customTypeHint: '自定義下載器類型,用於插件等場景',
|
||||
rtorrentHostHint: 'HTTP: http://ip:port/RPC2 或 SCGI: scgi://ip:port',
|
||||
enabled: '啟用',
|
||||
default: '預設',
|
||||
host: '地址',
|
||||
@@ -2790,6 +2875,15 @@ export default {
|
||||
password: '密碼',
|
||||
syncLibraries: '同步媒體庫',
|
||||
syncLibrariesHint: '只有選中的媒體庫才會被同步',
|
||||
scanMode: '掃描模式',
|
||||
scanModeHint: '用於全庫刷新和按庫刷新:新添加和修改 / 補充缺失 / 覆蓋掃描',
|
||||
verifySsl: '校驗 SSL 憑證',
|
||||
verifySslHint: '開啟後會校驗 HTTPS 憑證;如使用自簽憑證可關閉',
|
||||
scanModeOptions: {
|
||||
newAndModified: '新添加和修改',
|
||||
supplementMissing: '補充缺失',
|
||||
fullOverride: '覆蓋掃描',
|
||||
},
|
||||
nameExists: '【{name}】已存在,請替換為其他名稱',
|
||||
},
|
||||
bangumi: {
|
||||
@@ -3091,7 +3185,7 @@ export default {
|
||||
title: '媒體伺服器',
|
||||
description: '設定媒體伺服器',
|
||||
info: '媒體伺服器設定說明',
|
||||
infoDesc: '設定媒體伺服器用於媒體庫管理,可選擇Emby、Jellyfin或Plex等',
|
||||
infoDesc: '設定媒體伺服器用於媒體庫管理,可選擇Emby、Jellyfin、Plex、飛牛影視或綠聯影視',
|
||||
type: '媒體伺服器類型',
|
||||
typeHint: '選擇要使用的媒體伺服器類型',
|
||||
name: '伺服器名稱',
|
||||
|
||||
@@ -582,7 +582,7 @@ onUnmounted(() => {
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
autocomplete="username webauthn"
|
||||
autocomplete="username"
|
||||
:rules="[requiredValidator]"
|
||||
hide-details
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,6 @@ import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
|
||||
import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
|
||||
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
|
||||
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
|
||||
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
|
||||
import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue'
|
||||
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
|
||||
import { getSettingTabs } from '@/router/i18n-menu'
|
||||
@@ -93,15 +92,6 @@ onMounted(() => {
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 服务 -->
|
||||
<VWindowItem value="scheduler">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingService />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 通知 -->
|
||||
<VWindowItem value="notification">
|
||||
<transition name="fade-slide" appear>
|
||||
|
||||
@@ -185,12 +185,6 @@ export function getSettingTabs(t: Composer['t']) {
|
||||
tab: 'subscribe',
|
||||
description: t('settingTabs.subscribe.description'),
|
||||
},
|
||||
{
|
||||
title: t('settingTabs.scheduler.title'),
|
||||
icon: 'mdi-list-box',
|
||||
tab: 'scheduler',
|
||||
description: t('settingTabs.scheduler.description'),
|
||||
},
|
||||
{
|
||||
title: t('settingTabs.notification.title'),
|
||||
icon: 'mdi-bell',
|
||||
|
||||
@@ -80,9 +80,9 @@ async function fetchRemoteModules(): Promise<RemoteModule[]> {
|
||||
* @param modules 远程模块列表
|
||||
*/
|
||||
function injectRemoteModule(module: RemoteModule): void {
|
||||
// 从浏览器地址栏获取当前地址前缀
|
||||
// 与 API 请求一致:使用 origin + pathname 作为前缀,子路径代理时 pathname 含 /mp 等
|
||||
const baseUrl = new URL(window.location.href)
|
||||
// 环境变量
|
||||
const pathBase = baseUrl.pathname.replace(/\/$/, '') || ''
|
||||
let apiBase = import.meta.env.VITE_API_BASE_URL
|
||||
if (apiBase.startsWith('/')) {
|
||||
apiBase = apiBase.slice(1)
|
||||
@@ -90,8 +90,10 @@ function injectRemoteModule(module: RemoteModule): void {
|
||||
if (apiBase.endsWith('/')) {
|
||||
apiBase = apiBase.slice(0, -1)
|
||||
}
|
||||
const pathWithoutLeadingSlash = module.url.startsWith('/') ? module.url.slice(1) : module.url
|
||||
const remoteEntryUrl = `${baseUrl.origin}${pathBase}/${apiBase}/${pathWithoutLeadingSlash}`
|
||||
__federation_method_setRemote(module.id, {
|
||||
url: () => Promise.resolve(`${baseUrl.origin}/${apiBase}${module.url}`),
|
||||
url: () => Promise.resolve(remoteEntryUrl),
|
||||
format: 'esm',
|
||||
from: 'vite',
|
||||
})
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
// 导入所有 logo 图标
|
||||
import qbittorrentLogo from '@/assets/images/logos/qbittorrent.png'
|
||||
import transmissionLogo from '@/assets/images/logos/transmission.png'
|
||||
import rtorrentLogo from '@/assets/images/logos/rtorrent.png'
|
||||
import embyLogo from '@/assets/images/logos/emby.png'
|
||||
import jellyfinLogo from '@/assets/images/logos/jellyfin.png'
|
||||
import plexLogo from '@/assets/images/logos/plex.png'
|
||||
import trimemediaLogo from '@/assets/images/logos/trimemedia.png'
|
||||
import ugreenLogo from '@/assets/images/logos/ugreen.png'
|
||||
import wechatLogo from '@/assets/images/logos/wechat.png'
|
||||
import telegramLogo from '@/assets/images/logos/telegram.webp'
|
||||
import slackLogo from '@/assets/images/logos/slack.webp'
|
||||
@@ -29,15 +31,18 @@ import pluginLogo from '@/assets/images/logos/plugin.png'
|
||||
import siteLogo from '@/assets/images/logos/site.webp'
|
||||
import bangumiLogo from '@/assets/images/logos/bangumi.png'
|
||||
import doubanBlackLogo from '@/assets/images/logos/douban-black.png'
|
||||
import qqLogo from '@/assets/images/logos/qq.png'
|
||||
|
||||
// 图标映射表
|
||||
const logoMap: Record<string, string> = {
|
||||
qbittorrent: qbittorrentLogo,
|
||||
transmission: transmissionLogo,
|
||||
rtorrent: rtorrentLogo,
|
||||
emby: embyLogo,
|
||||
jellyfin: jellyfinLogo,
|
||||
plex: plexLogo,
|
||||
trimemedia: trimemediaLogo,
|
||||
ugreen: ugreenLogo,
|
||||
wechat: wechatLogo,
|
||||
telegram: telegramLogo,
|
||||
slack: slackLogo,
|
||||
@@ -57,6 +62,7 @@ const logoMap: Record<string, string> = {
|
||||
site: siteLogo,
|
||||
bangumi: bangumiLogo,
|
||||
'douban-black': doubanBlackLogo,
|
||||
qq: qqLogo,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,7 +28,7 @@ async function loadMediaStatistic() {
|
||||
},
|
||||
{
|
||||
title: t('dashboard.episodes'),
|
||||
stats: res.episode_count.toLocaleString(),
|
||||
stats: res.episode_count == null ? t('common.notFetched') : res.episode_count.toLocaleString(),
|
||||
icon: 'mdi-television-classic',
|
||||
color: 'warning',
|
||||
},
|
||||
|
||||
@@ -31,6 +31,9 @@ const isRefreshed = ref(false)
|
||||
const dataList = ref<MediaInfo[]>([])
|
||||
const currData = ref<MediaInfo[]>([])
|
||||
|
||||
// 用于保存已处理过的 key
|
||||
const seenKeys = ref<Set<string>>(new Set<string>())
|
||||
|
||||
// 拼装参数
|
||||
function getParams() {
|
||||
let params = {
|
||||
@@ -41,6 +44,31 @@ function getParams() {
|
||||
return params
|
||||
}
|
||||
|
||||
// MediaInfo 去重的字段
|
||||
const dedupFields = [
|
||||
"source",
|
||||
"type",
|
||||
"season",
|
||||
"tmdb_id",
|
||||
"imdb_id",
|
||||
"tvdb_id",
|
||||
"douban_id",
|
||||
"bangumi_id",
|
||||
"mediaid_prefix",
|
||||
"media_id",
|
||||
] as const;
|
||||
|
||||
function deduplicate(items: MediaInfo[]): MediaInfo[] {
|
||||
return items.filter(item => {
|
||||
const key = dedupFields.map(field => String(item[field])).join('~');
|
||||
if (seenKeys.value.has(key)) {
|
||||
return false;
|
||||
}
|
||||
seenKeys.value.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// 获取列表数据
|
||||
async function fetchData({ done }: { done: any }) {
|
||||
try {
|
||||
@@ -71,8 +99,10 @@ async function fetchData({ done }: { done: any }) {
|
||||
done('empty')
|
||||
return
|
||||
}
|
||||
// 去重
|
||||
currData.value = deduplicate(currData.value)
|
||||
// 合并数据
|
||||
dataList.value = [...dataList.value, ...currData.value]
|
||||
dataList.value.push(...currData.value)
|
||||
// 页码+1
|
||||
page.value++
|
||||
// 返回加载成功
|
||||
@@ -92,8 +122,10 @@ async function fetchData({ done }: { done: any }) {
|
||||
// 如果没有数据,跳出
|
||||
done('empty')
|
||||
} else {
|
||||
// 去重
|
||||
currData.value = deduplicate(currData.value)
|
||||
// 合并数据
|
||||
dataList.value = [...dataList.value, ...currData.value]
|
||||
dataList.value.push(...currData.value)
|
||||
// 页码+1
|
||||
page.value++
|
||||
// 返回加载成功
|
||||
|
||||
@@ -150,7 +150,8 @@ async function loadSeasonEpisodes(season: number) {
|
||||
// 加载季集信息
|
||||
if (seasonEpisodesInfo.value[season]) return
|
||||
try {
|
||||
const result: TmdbEpisode[] = await api.get(`tmdb/${mediaDetail.value.tmdb_id}/${season}`)
|
||||
const params = mediaDetail.value.episode_group ? { episode_group: mediaDetail.value.episode_group } : undefined
|
||||
const result: TmdbEpisode[] = await api.get(`tmdb/${mediaDetail.value.tmdb_id}/${season}`, params ? { params } : undefined)
|
||||
seasonEpisodesInfo.value[season] = result || []
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -189,7 +190,7 @@ async function checkExists() {
|
||||
}
|
||||
|
||||
// 查询当前媒体是否已订阅
|
||||
async function checkSubscribe(season = 0) {
|
||||
async function checkSubscribe(season: number | null = null) {
|
||||
try {
|
||||
const mediaid = getMediaId()
|
||||
|
||||
@@ -233,9 +234,14 @@ async function checkMovieSubscribed() {
|
||||
isSubscribed.value = await checkSubscribe()
|
||||
}
|
||||
|
||||
// 过滤掉第0季
|
||||
// 季列表,第0季排在最后
|
||||
const getMediaSeasons = computed(() => {
|
||||
return mediaDetail.value?.season_info?.filter(season => season.season_number !== 0)
|
||||
if (!mediaDetail.value?.season_info) return []
|
||||
return [...mediaDetail.value.season_info].sort((a, b) => {
|
||||
if (a.season_number === 0) return 1
|
||||
if (b.season_number === 0) return -1
|
||||
return (a.season_number || 0) - (b.season_number || 0)
|
||||
})
|
||||
})
|
||||
|
||||
// 检查所有季的订阅状态
|
||||
@@ -243,7 +249,7 @@ async function checkSeasonsSubscribed() {
|
||||
if (mediaDetail.value.type !== '电视剧') return
|
||||
try {
|
||||
mediaDetail.value?.season_info?.forEach(async item => {
|
||||
seasonsSubscribed.value[item.season_number ?? 0] = await checkSubscribe(item.season_number)
|
||||
seasonsSubscribed.value[item.season_number ?? 0] = await checkSubscribe(item.season_number ?? null)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -251,13 +257,13 @@ async function checkSeasonsSubscribed() {
|
||||
}
|
||||
|
||||
// 调用API添加订阅,电视剧的话需要指定季
|
||||
async function addSubscribe(season = 0) {
|
||||
async function addSubscribe(season: number | null) {
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
// 是否洗版
|
||||
let best_version = existsItemId.value ? 1 : 0
|
||||
if (season)
|
||||
if (season !== null)
|
||||
// 全部存在时洗版
|
||||
best_version = !seasonsNotExisted.value[season] ? 1 : 0
|
||||
// 请求API
|
||||
@@ -268,7 +274,7 @@ async function addSubscribe(season = 0) {
|
||||
tmdbid: mediaDetail.value?.tmdb_id,
|
||||
doubanid: mediaDetail.value?.douban_id,
|
||||
bangumiid: mediaDetail.value?.bangumi_id,
|
||||
season,
|
||||
season: mediaDetail.value?.type === '电影' ? null : season,
|
||||
best_version,
|
||||
})
|
||||
|
||||
@@ -276,7 +282,7 @@ async function addSubscribe(season = 0) {
|
||||
if (result.success) {
|
||||
// 订阅成功
|
||||
isSubscribed.value = true
|
||||
if (season) seasonsSubscribed.value[season] = true
|
||||
if (season !== null) seasonsSubscribed.value[season] = true
|
||||
}
|
||||
|
||||
// 提示
|
||||
@@ -297,8 +303,8 @@ async function addSubscribe(season = 0) {
|
||||
}
|
||||
|
||||
// 弹出添加订阅提示
|
||||
function showSubscribeAddToast(result: boolean, title: string, season: number, message: string, best_version: number) {
|
||||
if (season) title = `${title} ${formatSeason(season.toString())}`
|
||||
function showSubscribeAddToast(result: boolean, title: string, season: number | null, message: string, best_version: number) {
|
||||
if (season !== null) title = `${title} ${formatSeason(season.toString())}`
|
||||
|
||||
let subname = t('media.subscribe.normal')
|
||||
if (best_version > 0) subname = t('media.subscribe.bestVersion')
|
||||
@@ -307,7 +313,7 @@ function showSubscribeAddToast(result: boolean, title: string, season: number, m
|
||||
}
|
||||
|
||||
// 调用API取消订阅
|
||||
async function removeSubscribe(season: number) {
|
||||
async function removeSubscribe(season: number | null) {
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
@@ -321,7 +327,7 @@ async function removeSubscribe(season: number) {
|
||||
|
||||
if (result.success) {
|
||||
isSubscribed.value = false
|
||||
if (season) seasonsSubscribed.value[season] = false
|
||||
if (season !== null) seasonsSubscribed.value[season] = false
|
||||
$toast.success(`${mediaDetail.value?.title} ${t('media.subscribe.canceled')}`)
|
||||
} else {
|
||||
$toast.error(`${mediaDetail.value?.title} ${t('media.subscribe.cancelFailed', { reason: result.message })}`)
|
||||
@@ -333,7 +339,7 @@ async function removeSubscribe(season: number) {
|
||||
}
|
||||
|
||||
// 订阅按钮响应
|
||||
function handleSubscribe(season = 0) {
|
||||
function handleSubscribe(season: number | null = null) {
|
||||
if (isSubscribed.value) removeSubscribe(season)
|
||||
else addSubscribe(season)
|
||||
}
|
||||
@@ -641,7 +647,7 @@ onBeforeMount(() => {
|
||||
class="ms-2 mb-2"
|
||||
:color="getSubscribeColor"
|
||||
variant="tonal"
|
||||
@click="handleSubscribe(0)"
|
||||
@click="handleSubscribe()"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="getSubscribeIcon" />
|
||||
@@ -741,8 +747,9 @@ onBeforeMount(() => {
|
||||
<template #default>
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<span class="font-weight-bold">{{
|
||||
t('media.seasonNumber', { number: season.season_number })
|
||||
}}</span>
|
||||
season.season_number === 0 && season.name ?
|
||||
season.name : t('media.seasonNumber', { number: season.season_number })
|
||||
}}</span>
|
||||
<VChip size="small" class="ms-1">
|
||||
{{ t('media.episodeCount', { count: season.episode_count }) }}
|
||||
</VChip>
|
||||
@@ -754,7 +761,7 @@ onBeforeMount(() => {
|
||||
class="ms-1"
|
||||
:color="seasonsSubscribed[season.season_number || 0] ? 'error' : 'warning'"
|
||||
variant="text"
|
||||
@click.stop="handleSubscribe(season.season_number)"
|
||||
@click.stop="handleSubscribe(season.season_number ?? null)"
|
||||
>
|
||||
<VIcon
|
||||
:icon="seasonsSubscribed[season.season_number || 0] ? 'mdi-heart' : 'mdi-heart-outline'"
|
||||
|
||||
@@ -177,7 +177,7 @@ const loading = ref(false)
|
||||
const totalItems = ref(0)
|
||||
|
||||
// 是否要分组
|
||||
const group = ref(false)
|
||||
const group = ref<boolean>(route.query.grouped === 'true')
|
||||
|
||||
// 分组条件
|
||||
const groupBy = ref<any>([
|
||||
@@ -487,6 +487,9 @@ function reloadPage(resetPage = false) {
|
||||
if (currentPage.value) {
|
||||
url = addUrlQuery(url, 'currentPage', resetPage ? 1 : currentPage.value)
|
||||
}
|
||||
if (group.value) {
|
||||
url = addUrlQuery(url, 'grouped', 'true')
|
||||
}
|
||||
router.push(url)
|
||||
}
|
||||
|
||||
@@ -500,6 +503,26 @@ function ensureNumber(value: any, defaultValue: number = 0) {
|
||||
return value
|
||||
}
|
||||
|
||||
// 按标题分组后的选中数量统计,键为标题,值为对应分组的选中数
|
||||
const selectedCountsGroupedByTitle = computed(() => {
|
||||
return selected.value.reduce((acc, item) => {
|
||||
const title = item.title || '';
|
||||
acc[title] = (acc[title] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
});
|
||||
|
||||
// 控制分组内所有子项的选中状态
|
||||
const toggleGroupSelection = (checked: boolean | null, items: readonly any[]) => {
|
||||
const values = items.map(item => item.value);
|
||||
if (checked) {
|
||||
selected.value = [...new Set([...selected.value, ...values])];
|
||||
} else {
|
||||
const itemsSet = new Set(values);
|
||||
selected.value = selected.value.filter(item => !itemsSet.has(item));
|
||||
}
|
||||
};
|
||||
|
||||
// 初始加载数据
|
||||
onMounted(() => {
|
||||
loadStorages()
|
||||
@@ -563,13 +586,20 @@ onMounted(() => {
|
||||
<template v-slot:group-header="{ item, columns, toggleGroup, isGroupOpen }">
|
||||
<tr>
|
||||
<td :colspan="columns.length">
|
||||
<VBtn
|
||||
:icon="isGroupOpen(item) ? '$expand' : '$next'"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="toggleGroup(item)"
|
||||
/>
|
||||
{{ item.value }}
|
||||
<div class="d-flex align-center gap-2">
|
||||
<VBtn
|
||||
:icon="isGroupOpen(item) ? '$expand' : '$next'"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="toggleGroup(item)"
|
||||
/>
|
||||
<VCheckbox
|
||||
:model-value="selectedCountsGroupedByTitle[item.value] == item.items.length"
|
||||
:indeterminate="selectedCountsGroupedByTitle[item.value] < item.items.length"
|
||||
@update:modelValue="(checked) => toggleGroupSelection(checked, item.items)"
|
||||
/>
|
||||
{{ item.value }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TransferDirectoryConf, StorageConf } from '@/api/types'
|
||||
import DirectoryCard from '@/components/cards/DirectoryCard.vue'
|
||||
import StorageCard from '@/components/cards/StorageCard.vue'
|
||||
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
|
||||
import CategoryEditDialog from '@/components/dialog/CategoryEditDialog.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { storageAttributes } from '@/api/constants'
|
||||
|
||||
@@ -28,6 +29,9 @@ const $toast = useToast()
|
||||
// 进度框
|
||||
const progressDialog = ref(false)
|
||||
|
||||
// 分类编辑对话框
|
||||
const categoryDialog = ref(false)
|
||||
|
||||
// 数据源
|
||||
const sourceItems = [
|
||||
{ 'title': 'TheMovieDb', 'value': 'themoviedb' },
|
||||
@@ -292,7 +296,12 @@ onMounted(() => {
|
||||
:directory="element"
|
||||
:categories="mediaCategories"
|
||||
:storages="storages"
|
||||
@update:modelValue="(value: any) => {element.download_path = value?.download; element.library_path = value?.library}"
|
||||
@update:modelValue="
|
||||
(value: any) => {
|
||||
element.download_path = value?.download
|
||||
element.library_path = value?.library
|
||||
}
|
||||
"
|
||||
@close="removeDirectory(element)"
|
||||
/>
|
||||
</template>
|
||||
@@ -304,9 +313,13 @@ onMounted(() => {
|
||||
<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" class="me-2">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn color="info" variant="tonal" prepend-icon="mdi-shape-plus" @click="categoryDialog = true">
|
||||
{{ t('setting.category.title') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -370,4 +383,12 @@ onMounted(() => {
|
||||
</VRow>
|
||||
<!-- 进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="t('setting.system.reloading')" />
|
||||
<!-- 分类对话框 -->
|
||||
<CategoryEditDialog
|
||||
v-if="categoryDialog"
|
||||
v-model="categoryDialog"
|
||||
:categories="mediaCategories"
|
||||
@close="categoryDialog = false"
|
||||
@done="loadMediaCategories"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -300,6 +300,9 @@ onMounted(() => {
|
||||
<VListItem @click="addNotification('synologychat')">
|
||||
<VListItemTitle>{{ t('setting.notification.synologyChat') }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem @click="addNotification('qqbot')">
|
||||
<VListItemTitle>{{ t('setting.notification.qq') }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem @click="addNotification('vocechat')">
|
||||
<VListItemTitle>{{ t('setting.notification.voceChat') }}</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
@@ -33,6 +33,8 @@ const SystemSettings = ref<any>({
|
||||
CUSTOMIZE_WALLPAPER_API_URL: null,
|
||||
AI_AGENT_ENABLE: false,
|
||||
AI_AGENT_GLOBAL: false,
|
||||
AI_AGENT_VERBOSE: false,
|
||||
AI_AGENT_JOB_INTERVAL: 24,
|
||||
LLM_PROVIDER: 'deepseek',
|
||||
LLM_MODEL: 'deepseek-chat',
|
||||
LLM_API_KEY: null,
|
||||
@@ -40,6 +42,7 @@ const SystemSettings = ref<any>({
|
||||
AI_RECOMMEND_ENABLED: false,
|
||||
AI_RECOMMEND_USER_PREFERENCE: null,
|
||||
AI_RECOMMEND_MAX_ITEMS: 50,
|
||||
LLM_MAX_CONTEXT_TOKENS: 64,
|
||||
},
|
||||
// 高级系统设置
|
||||
Advanced: {
|
||||
@@ -54,6 +57,7 @@ const SystemSettings = ref<any>({
|
||||
AUTO_UPDATE_RESOURCE: true,
|
||||
MOVIEPILOT_AUTO_UPDATE: false,
|
||||
// 媒体
|
||||
RECOGNIZE_PLUGIN_FIRST: false,
|
||||
TMDB_API_DOMAIN: null,
|
||||
TMDB_IMAGE_DOMAIN: null,
|
||||
TMDB_LOCALE: null,
|
||||
@@ -83,28 +87,53 @@ const SystemSettings = ref<any>({
|
||||
},
|
||||
})
|
||||
|
||||
// 刮削开关设置
|
||||
const ScrapingSwitchs = ref<any>({
|
||||
movie_nfo: true, // 电影NFO
|
||||
movie_poster: true, // 电影海报
|
||||
movie_backdrop: true, // 电影背景图
|
||||
movie_logo: true, // 电影Logo
|
||||
movie_disc: true, // 电影光盘图
|
||||
movie_banner: true, // 电影横幅图
|
||||
movie_thumb: true, // 电影缩略图
|
||||
tv_nfo: true, // 电视剧NFO
|
||||
tv_poster: true, // 电视剧海报
|
||||
tv_backdrop: true, // 电视剧背景图
|
||||
tv_banner: true, // 电视剧横幅图
|
||||
tv_logo: true, // 电视剧Logo
|
||||
tv_thumb: true, // 电视剧缩略图
|
||||
season_nfo: true, // 季NFO
|
||||
season_poster: true, // 季海报
|
||||
season_banner: true, // 季横幅图
|
||||
season_thumb: true, // 季缩略图
|
||||
episode_nfo: true, // 集NFO
|
||||
episode_thumb: true, // 集缩略图
|
||||
})
|
||||
// 刮削配置
|
||||
const scrapingConfig = [
|
||||
{
|
||||
section: 'movie',
|
||||
items: [
|
||||
{ key: 'movie_nfo', label: 'setting.system.movieNfo' },
|
||||
{ key: 'movie_poster', label: 'setting.system.moviePoster' },
|
||||
{ key: 'movie_backdrop', label: 'setting.system.movieBackdrop' },
|
||||
{ key: 'movie_logo', label: 'setting.system.movieLogo' },
|
||||
{ key: 'movie_disc', label: 'setting.system.movieDisc' },
|
||||
{ key: 'movie_banner', label: 'setting.system.movieBanner' },
|
||||
{ key: 'movie_thumb', label: 'setting.system.movieThumb' },
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'tv',
|
||||
items: [
|
||||
{ key: 'tv_nfo', label: 'setting.system.tvNfo' },
|
||||
{ key: 'tv_poster', label: 'setting.system.tvPoster' },
|
||||
{ key: 'tv_backdrop', label: 'setting.system.tvBackdrop' },
|
||||
{ key: 'tv_banner', label: 'setting.system.tvBanner' },
|
||||
{ key: 'tv_logo', label: 'setting.system.tvLogo' },
|
||||
{ key: 'tv_thumb', label: 'setting.system.tvThumb' },
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'season',
|
||||
items: [
|
||||
{ key: 'season_nfo', label: 'setting.system.seasonNfo' },
|
||||
{ key: 'season_poster', label: 'setting.system.seasonPoster' },
|
||||
{ key: 'season_banner', label: 'setting.system.seasonBanner' },
|
||||
{ key: 'season_thumb', label: 'setting.system.seasonThumb' },
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'episode',
|
||||
items: [
|
||||
{ key: 'episode_nfo', label: 'setting.system.episodeNfo' },
|
||||
{ key: 'episode_thumb', label: 'setting.system.episodeThumb' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// 刮削策略设置
|
||||
const ScrapingPolicies = ref<Record<string, 'skip' | 'missingOnly' | 'overwrite'>>(
|
||||
Object.fromEntries(scrapingConfig.flatMap(section => section.items.map(item => [item.key, 'missingOnly']))),
|
||||
)
|
||||
|
||||
// 是否发送请求的总开关
|
||||
const isRequest = ref(true)
|
||||
@@ -479,7 +508,14 @@ async function loadScrapingSwitchs() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/ScrapingSwitchs')
|
||||
if (result.success && result.data?.value) {
|
||||
ScrapingSwitchs.value = { ...ScrapingSwitchs.value, ...result.data.value }
|
||||
const loadedSwitches = result.data.value
|
||||
for (const key in loadedSwitches) {
|
||||
if (typeof loadedSwitches[key] === 'boolean') {
|
||||
// 兼容旧数据
|
||||
loadedSwitches[key] = loadedSwitches[key] ? 'missingOnly' : 'skip'
|
||||
}
|
||||
}
|
||||
ScrapingPolicies.value = { ...ScrapingPolicies.value, ...loadedSwitches }
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
@@ -489,7 +525,7 @@ async function loadScrapingSwitchs() {
|
||||
// 保存刮削开关设置
|
||||
async function saveScrapingSwitchs() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('system/setting/ScrapingSwitchs', ScrapingSwitchs.value)
|
||||
const result: { [key: string]: any } = await api.post('system/setting/ScrapingSwitchs', ScrapingPolicies.value)
|
||||
if (result.success) {
|
||||
return true
|
||||
} else {
|
||||
@@ -647,7 +683,7 @@ onDeactivated(() => {
|
||||
</VRow>
|
||||
<VDivider class="my-4" />
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCol cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Basic.AI_AGENT_ENABLE"
|
||||
:label="t('setting.system.aiAgentEnable')"
|
||||
@@ -655,6 +691,22 @@ onDeactivated(() => {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Basic.AI_AGENT_GLOBAL"
|
||||
:label="t('setting.system.aiAgentGlobal')"
|
||||
:hint="t('setting.system.aiAgentGlobalHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Basic.AI_AGENT_VERBOSE"
|
||||
:label="t('setting.system.aiAgentVerbose')"
|
||||
:hint="t('setting.system.aiAgentVerboseHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="SystemSettings.Basic.LLM_PROVIDER"
|
||||
@@ -713,14 +765,35 @@ onDeactivated(() => {
|
||||
</VCombobox>
|
||||
</VCol>
|
||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Basic.AI_AGENT_GLOBAL"
|
||||
:label="t('setting.system.aiAgentGlobal')"
|
||||
:hint="t('setting.system.aiAgentGlobalHint')"
|
||||
<VTextField
|
||||
v-model.number="SystemSettings.Basic.LLM_MAX_CONTEXT_TOKENS"
|
||||
:label="t('setting.system.llmMaxContextTokens')"
|
||||
:hint="t('setting.system.llmMaxContextTokensHint')"
|
||||
persistent-hint
|
||||
type="number"
|
||||
prepend-inner-icon="mdi-counter"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="SystemSettings.Basic.AI_AGENT_JOB_INTERVAL"
|
||||
:label="t('setting.system.aiAgentJobInterval')"
|
||||
:hint="t('setting.system.aiAgentJobIntervalHint')"
|
||||
persistent-hint
|
||||
:items="[
|
||||
{ title: t('setting.system.aiAgentJobIntervalDisabled'), value: 0 },
|
||||
{ title: t('setting.system.aiAgentJobInterval1h'), value: 1 },
|
||||
{ title: t('setting.system.aiAgentJobInterval3h'), value: 3 },
|
||||
{ title: t('setting.system.aiAgentJobInterval6h'), value: 6 },
|
||||
{ title: t('setting.system.aiAgentJobInterval12h'), value: 12 },
|
||||
{ title: t('setting.system.aiAgentJobInterval24h'), value: 24 },
|
||||
{ title: t('setting.system.aiAgentJobInterval1w'), value: 168 },
|
||||
{ title: t('setting.system.aiAgentJobInterval1M'), value: 720 },
|
||||
]"
|
||||
prepend-inner-icon="mdi-timer-outline"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Basic.AI_RECOMMEND_ENABLED"
|
||||
:label="t('setting.system.aiRecommendEnabled')"
|
||||
@@ -728,7 +801,11 @@ onDeactivated(() => {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && SystemSettings.Basic.AI_RECOMMEND_ENABLED" cols="12">
|
||||
<VCol
|
||||
v-if="SystemSettings.Basic.AI_AGENT_ENABLE && SystemSettings.Basic.AI_RECOMMEND_ENABLED"
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextarea
|
||||
v-model="SystemSettings.Basic.AI_RECOMMEND_USER_PREFERENCE"
|
||||
:label="t('setting.system.aiRecommendUserPreference')"
|
||||
@@ -1087,6 +1164,16 @@ onDeactivated(() => {
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.RECOGNIZE_PLUGIN_FIRST"
|
||||
:label="t('setting.system.recognizePluginFirst')"
|
||||
:hint="t('setting.system.recognizePluginFirstHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<!-- 刮削开关设置 -->
|
||||
<VRow class="mt-4">
|
||||
@@ -1094,173 +1181,67 @@ onDeactivated(() => {
|
||||
<VExpansionPanels>
|
||||
<VExpansionPanel>
|
||||
<VExpansionPanelTitle class="text-lg">
|
||||
<template #default>
|
||||
<VIcon icon="mdi-checkbox-multiple-outline" class="me-2" />
|
||||
{{ t('setting.system.scrapingSwitchSettings') }}
|
||||
</template>
|
||||
<VIcon icon="mdi-checkbox-multiple-outline" class="me-2" />
|
||||
{{ t('setting.system.scrapingSwitchSettings') }}
|
||||
<!-- 帮助图标 -->
|
||||
<VTooltip location="bottom" open-delay="200">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<VBtn
|
||||
v-bind="tooltipProps"
|
||||
icon="mdi-help-circle"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="medium-emphasis"
|
||||
class="ml-2"
|
||||
@click.stop
|
||||
/>
|
||||
</template>
|
||||
<div class="d-flex flex-column gap-2 py-2">
|
||||
<div class="d-flex align-center">
|
||||
<VIcon icon="mdi-file-remove" color="error" class="mr-2" />
|
||||
<span>{{ t('setting.system.policy.skipDesc') }}</span>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<VIcon icon="mdi-file-plus" color="success" class="mr-2" />
|
||||
<span>{{ t('setting.system.policy.missingOnlyDesc') }}</span>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<VIcon icon="mdi-file-replace" color="primary" class="mr-2" />
|
||||
<span>{{ t('setting.system.policy.overwriteDesc') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</VTooltip>
|
||||
</VExpansionPanelTitle>
|
||||
<VExpansionPanelText>
|
||||
<VRow>
|
||||
<VRow v-for="section in scrapingConfig" :key="section.section">
|
||||
<VCol cols="12" class="pb-2">
|
||||
<VListSubheader class="text-lg">{{ t('setting.system.movie') }}</VListSubheader>
|
||||
<VListSubheader class="text-lg">
|
||||
{{ t(`setting.system.${section.section}`) }}
|
||||
</VListSubheader>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.movie_nfo"
|
||||
:label="t('setting.system.movieNfo')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.movie_poster"
|
||||
:label="t('setting.system.moviePoster')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.movie_backdrop"
|
||||
:label="t('setting.system.movieBackdrop')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.movie_logo"
|
||||
:label="t('setting.system.movieLogo')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.movie_disc"
|
||||
:label="t('setting.system.movieDisc')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.movie_banner"
|
||||
:label="t('setting.system.movieBanner')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.movie_thumb"
|
||||
:label="t('setting.system.movieThumb')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<VDivider class="my-4" />
|
||||
|
||||
<VRow>
|
||||
<VCol cols="12" class="pb-2">
|
||||
<VListSubheader class="text-lg">{{ t('setting.system.tv') }}</VListSubheader>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.tv_nfo"
|
||||
:label="t('setting.system.tvNfo')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.tv_poster"
|
||||
:label="t('setting.system.tvPoster')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.tv_backdrop"
|
||||
:label="t('setting.system.tvBackdrop')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.tv_banner"
|
||||
:label="t('setting.system.tvBanner')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.tv_logo"
|
||||
:label="t('setting.system.tvLogo')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.tv_thumb"
|
||||
:label="t('setting.system.tvThumb')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<VDivider class="my-4" />
|
||||
|
||||
<VRow>
|
||||
<VCol cols="12" class="pb-2">
|
||||
<VListSubheader class="text-lg">{{ t('setting.system.season') }}</VListSubheader>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.season_nfo"
|
||||
:label="t('setting.system.seasonNfo')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.season_poster"
|
||||
:label="t('setting.system.seasonPoster')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.season_banner"
|
||||
:label="t('setting.system.seasonBanner')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.season_thumb"
|
||||
:label="t('setting.system.seasonThumb')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<VDivider class="my-4" />
|
||||
|
||||
<VRow>
|
||||
<VCol cols="12" class="pb-2">
|
||||
<VListSubheader class="text-lg">{{ t('setting.system.episode') }}</VListSubheader>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.episode_nfo"
|
||||
:label="t('setting.system.episodeNfo')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.episode_thumb"
|
||||
:label="t('setting.system.episodeThumb')"
|
||||
density="compact"
|
||||
/>
|
||||
<VCol v-for="item in section.items" :key="item.key" cols="12" md="4">
|
||||
<div class="d-flex align-center">
|
||||
<VBtnToggle
|
||||
:model-value="ScrapingPolicies[item.key]"
|
||||
@update:model-value="ScrapingPolicies[item.key] = $event"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
rounded="lg"
|
||||
>
|
||||
<VBtn value="skip" color="error">
|
||||
<VIcon icon="mdi-file-remove" />
|
||||
</VBtn>
|
||||
<VBtn value="missingOnly" color="success">
|
||||
<VIcon icon="mdi-file-plus" />
|
||||
</VBtn>
|
||||
<VBtn value="overwrite" color="primary">
|
||||
<VIcon icon="mdi-file-replace" />
|
||||
</VBtn>
|
||||
</VBtnToggle>
|
||||
<span class="ml-2">{{ t(item.label) }}</span>
|
||||
</div>
|
||||
</VCol>
|
||||
<VDivider v-if="section.section !== 'episode'" class="my-4" />
|
||||
</VRow>
|
||||
</VExpansionPanelText>
|
||||
</VExpansionPanel>
|
||||
@@ -1414,7 +1395,10 @@ onDeactivated(() => {
|
||||
min="1"
|
||||
type="number"
|
||||
: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>
|
||||
@@ -1426,7 +1410,10 @@ onDeactivated(() => {
|
||||
persistent-hint
|
||||
min="1"
|
||||
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>
|
||||
|
||||
@@ -27,7 +27,7 @@ const { wizardData, selectDownloader, validationErrors } = useSetupWizard()
|
||||
<div class="mb-4">
|
||||
<h4 class="text-h6 mb-4">{{ t('setupWizard.downloader.type') }}</h4>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VCol cols="12" md="4">
|
||||
<VCard
|
||||
:color="wizardData.downloader.type === 'qbittorrent' ? 'primary' : 'default'"
|
||||
:variant="wizardData.downloader.type === 'qbittorrent' ? 'tonal' : 'outlined'"
|
||||
@@ -40,7 +40,7 @@ const { wizardData, selectDownloader, validationErrors } = useSetupWizard()
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VCol cols="12" md="4">
|
||||
<VCard
|
||||
:color="wizardData.downloader.type === 'transmission' ? 'primary' : 'default'"
|
||||
:variant="wizardData.downloader.type === 'transmission' ? 'tonal' : 'outlined'"
|
||||
@@ -53,6 +53,19 @@ const { wizardData, selectDownloader, validationErrors } = useSetupWizard()
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VCard
|
||||
:color="wizardData.downloader.type === 'rtorrent' ? 'primary' : 'default'"
|
||||
:variant="wizardData.downloader.type === 'rtorrent' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectDownloader('rtorrent')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg :src="getLogoUrl('rtorrent')" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">rTorrent</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VCol>
|
||||
@@ -203,6 +216,63 @@ const { wizardData, selectDownloader, validationErrors } = useSetupWizard()
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.downloader.type === 'rtorrent'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.name"
|
||||
:label="t('downloader.name')"
|
||||
:placeholder="t('downloader.nameRequired')"
|
||||
:hint="t('downloader.name')"
|
||||
:error="validationErrors.downloader.name"
|
||||
:error-messages="validationErrors.downloader.name ? [t('downloader.nameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.config.host"
|
||||
:label="t('downloader.host')"
|
||||
placeholder="http(s)://ip:port/RPC2"
|
||||
:hint="t('downloader.rtorrentHostHint')"
|
||||
:error="validationErrors.downloader.host"
|
||||
:error-messages="validationErrors.downloader.host ? [t('downloader.hostRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.config.username"
|
||||
:label="t('downloader.username')"
|
||||
:hint="t('downloader.username')"
|
||||
:error="validationErrors.downloader.username"
|
||||
:error-messages="validationErrors.downloader.username ? [t('downloader.usernameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.config.password"
|
||||
type="password"
|
||||
:label="t('downloader.password')"
|
||||
:hint="t('downloader.password')"
|
||||
:error="validationErrors.downloader.password"
|
||||
:error-messages="validationErrors.downloader.password ? [t('downloader.passwordRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
|
||||
@@ -15,6 +15,23 @@ const librariesOptions = ref<{ title: string; value: string | undefined }[]>([
|
||||
},
|
||||
])
|
||||
|
||||
const ugreenScanModeOptions = computed(() => [
|
||||
{ title: t('mediaserver.scanModeOptions.newAndModified'), value: 'new_and_modified' },
|
||||
{ title: t('mediaserver.scanModeOptions.supplementMissing'), value: 'supplement_missing' },
|
||||
{ title: t('mediaserver.scanModeOptions.fullOverride'), value: 'full_override' },
|
||||
])
|
||||
|
||||
function ensureUgreenConfig() {
|
||||
if (wizardData.value.mediaServer.type !== 'ugreen') return
|
||||
wizardData.value.mediaServer.config = wizardData.value.mediaServer.config || {}
|
||||
if (!wizardData.value.mediaServer.config.scan_mode) {
|
||||
wizardData.value.mediaServer.config.scan_mode = 'supplement_missing'
|
||||
}
|
||||
if (wizardData.value.mediaServer.config.verify_ssl === undefined) {
|
||||
wizardData.value.mediaServer.config.verify_ssl = true
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API查询媒体库
|
||||
async function loadLibrary(server: string) {
|
||||
try {
|
||||
@@ -42,6 +59,7 @@ async function loadLibrary(server: string) {
|
||||
// 选择媒体服务器并自动加载媒体库
|
||||
async function selectMediaServerWithLibrary(type: string) {
|
||||
selectMediaServer(type)
|
||||
ensureUgreenConfig()
|
||||
// 如果选择了媒体服务器类型,自动加载媒体库
|
||||
if (type && wizardData.value.mediaServer.name) {
|
||||
await loadLibrary(wizardData.value.mediaServer.name)
|
||||
@@ -50,6 +68,7 @@ async function selectMediaServerWithLibrary(type: string) {
|
||||
|
||||
// 组件挂载时检查是否需要加载媒体库
|
||||
onMounted(async () => {
|
||||
ensureUgreenConfig()
|
||||
// 如果已经有媒体服务器配置,自动加载媒体库
|
||||
if (wizardData.value.mediaServer.type && wizardData.value.mediaServer.name) {
|
||||
await loadLibrary(wizardData.value.mediaServer.name)
|
||||
@@ -60,6 +79,7 @@ onMounted(async () => {
|
||||
watch(
|
||||
() => [wizardData.value.mediaServer.type, wizardData.value.mediaServer.name],
|
||||
async ([type, name]) => {
|
||||
ensureUgreenConfig()
|
||||
console.log('Media server changed:', { type, name })
|
||||
if (type && name) {
|
||||
await loadLibrary(name)
|
||||
@@ -141,6 +161,19 @@ watch(
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.mediaServer.type === 'ugreen' ? 'primary' : 'default'"
|
||||
:variant="wizardData.mediaServer.type === 'ugreen' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectMediaServerWithLibrary('ugreen')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg :src="getLogoUrl('ugreen')" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">绿联影视</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VCol>
|
||||
@@ -380,6 +413,107 @@ watch(
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.mediaServer.type === 'ugreen'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.name"
|
||||
:label="t('common.name')"
|
||||
:placeholder="t('mediaserver.nameRequired')"
|
||||
:hint="t('mediaserver.serverAlias')"
|
||||
:error="validationErrors.mediaServer.name"
|
||||
:error-messages="validationErrors.mediaServer.name ? [t('mediaserver.nameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.host"
|
||||
:label="t('mediaserver.host')"
|
||||
:placeholder="t('mediaserver.hostPlaceholder')"
|
||||
:hint="t('mediaserver.hostHint')"
|
||||
:error="validationErrors.mediaServer.host"
|
||||
:error-messages="validationErrors.mediaServer.host ? [t('mediaserver.hostRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.play_host"
|
||||
:label="t('mediaserver.playHost')"
|
||||
:placeholder="t('mediaserver.playHostPlaceholder')"
|
||||
:hint="t('mediaserver.playHostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-play-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.username"
|
||||
:label="t('mediaserver.username')"
|
||||
:error="validationErrors.mediaServer.username"
|
||||
:error-messages="validationErrors.mediaServer.username ? [t('mediaserver.usernameRequired')] : []"
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
type="password"
|
||||
v-model="wizardData.mediaServer.config.password"
|
||||
:label="t('mediaserver.password')"
|
||||
:error="validationErrors.mediaServer.password"
|
||||
:error-messages="validationErrors.mediaServer.password ? [t('mediaserver.passwordRequired')] : []"
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VAutocomplete
|
||||
v-model="wizardData.mediaServer.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
chips
|
||||
multiple
|
||||
clearable
|
||||
:hint="t('mediaserver.syncLibrariesHint')"
|
||||
persistent-hint
|
||||
active
|
||||
append-inner-icon="mdi-refresh"
|
||||
prepend-inner-icon="mdi-library"
|
||||
@click:append-inner="loadLibrary(wizardData.mediaServer.name)"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="wizardData.mediaServer.config.scan_mode"
|
||||
:label="t('mediaserver.scanMode')"
|
||||
:items="ugreenScanModeOptions"
|
||||
:hint="t('mediaserver.scanModeHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-radar"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="wizardData.mediaServer.config.verify_ssl"
|
||||
:label="t('mediaserver.verifySsl')"
|
||||
:hint="t('mediaserver.verifySslHint')"
|
||||
persistent-hint
|
||||
color="primary"
|
||||
inset
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.mediaServer.type === 'plex'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
|
||||
@@ -91,6 +91,19 @@ const notificationTypes = [
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.notification.type === 'qqbot' ? 'primary' : 'default'"
|
||||
:variant="wizardData.notification.type === 'qqbot' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectNotification('qqbot')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg :src="getLogoUrl('notification')" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">QQ</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.notification.type === 'vocechat' ? 'primary' : 'default'"
|
||||
@@ -312,6 +325,59 @@ const notificationTypes = [
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.notification.type === 'qqbot'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.name"
|
||||
:label="t('notification.name')"
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
:error="validationErrors.notification.name"
|
||||
:error-messages="validationErrors.notification.name ? [t('notification.nameRequired')] : []"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.QQ_APP_ID"
|
||||
:label="t('notification.qqbot.appId')"
|
||||
:hint="t('notification.qqbot.appIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-application"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.QQ_APP_SECRET"
|
||||
:label="t('notification.qqbot.appSecret')"
|
||||
:hint="t('notification.qqbot.appSecretHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.QQ_OPENID"
|
||||
:label="t('notification.qqbot.openId')"
|
||||
:placeholder="t('notification.qqbot.openIdPlaceholder')"
|
||||
:hint="t('notification.qqbot.openIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.QQ_GROUP_OPENID"
|
||||
:label="t('notification.qqbot.groupOpenId')"
|
||||
:placeholder="t('notification.qqbot.groupOpenIdPlaceholder')"
|
||||
:hint="t('notification.qqbot.groupOpenIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-group"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.notification.type === 'slack'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
|
||||
@@ -71,7 +71,8 @@ async function eventsHander(subscribe: Subscribe) {
|
||||
}
|
||||
} else {
|
||||
// 调用API查询集信息
|
||||
const episodes: TmdbEpisode[] = await api.get(`tmdb/${subscribe.tmdbid}/${subscribe.season}`)
|
||||
const params = subscribe.episode_group ? { episode_group: subscribe.episode_group } : undefined
|
||||
const episodes: TmdbEpisode[] = await api.get(`tmdb/${subscribe.tmdbid}/${subscribe.season}`, params ? { params } : undefined)
|
||||
|
||||
interface EpisodeInfo {
|
||||
title: string
|
||||
|
||||
@@ -70,10 +70,6 @@ useDataRefresh(
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('setting.scheduler.title') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('setting.scheduler.subtitle') }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VTable class="text-no-wrap">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -132,6 +132,7 @@ async function fetchUserInfo() {
|
||||
if (result) {
|
||||
accountInfo.value = result
|
||||
accountInfo.value.avatar = accountInfo.value.avatar ? accountInfo.value.avatar : avatar1
|
||||
accountInfo.value.nickname = accountInfo.value.settings?.nickname ?? ''
|
||||
currentUserName.value = accountInfo.value.name
|
||||
currentAvatar.value = accountInfo.value.avatar
|
||||
// 同时加载PassKey列表
|
||||
@@ -161,12 +162,10 @@ async function saveAccountInfo() {
|
||||
}
|
||||
|
||||
// 将nickname保存到settings中,后端可以直接处理JSON对象
|
||||
if (accountInfo.value.nickname) {
|
||||
if (!accountInfo.value.settings) {
|
||||
accountInfo.value.settings = {}
|
||||
}
|
||||
accountInfo.value.settings.nickname = accountInfo.value.nickname
|
||||
if (!accountInfo.value.settings) {
|
||||
accountInfo.value.settings = {}
|
||||
}
|
||||
accountInfo.value.settings.nickname = accountInfo.value.nickname ?? ''
|
||||
|
||||
const oldUserName = accountInfo.value.name
|
||||
const oldAvatar = accountInfo.value.avatar
|
||||
|
||||
94
yarn.lock
94
yarn.lock
@@ -861,24 +861,24 @@
|
||||
integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==
|
||||
|
||||
"@emnapi/core@^1.4.3":
|
||||
version "1.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.4.5.tgz#bfbb0cbbbb9f96ec4e2c4fd917b7bbe5495ceccb"
|
||||
integrity sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.8.1.tgz#fd9efe721a616288345ffee17a1f26ac5dd01349"
|
||||
integrity sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==
|
||||
dependencies:
|
||||
"@emnapi/wasi-threads" "1.0.4"
|
||||
"@emnapi/wasi-threads" "1.1.0"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emnapi/runtime@^1.2.0", "@emnapi/runtime@^1.4.3":
|
||||
version "1.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.4.5.tgz#c67710d0661070f38418b6474584f159de38aba9"
|
||||
integrity sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.8.1.tgz#550fa7e3c0d49c5fb175a116e8cd70614f9a22a5"
|
||||
integrity sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emnapi/wasi-threads@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz#703fc094d969e273b1b71c292523b2f792862bf4"
|
||||
integrity sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==
|
||||
"@emnapi/wasi-threads@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf"
|
||||
integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
@@ -1147,21 +1147,21 @@
|
||||
|
||||
"@iconify-json/line-md@^1.2.13":
|
||||
version "1.2.13"
|
||||
resolved "https://registry.yarnpkg.com/@iconify-json/line-md/-/line-md-1.2.13.tgz#19714b8471ebac5871e20036512eaffa869a04b7"
|
||||
resolved "https://registry.npmjs.org/@iconify-json/line-md/-/line-md-1.2.13.tgz"
|
||||
integrity sha512-XFXThXsEQ2Wzzn+ze2T1d+JHkkFvI1AxiVKnOox4qFbdR9EVikckZlUK+/DUsV4zSy6pMQAgXpIk+1xG8qFYPQ==
|
||||
dependencies:
|
||||
"@iconify/types" "*"
|
||||
|
||||
"@iconify-json/lucide@^1.2.85":
|
||||
version "1.2.85"
|
||||
resolved "https://registry.yarnpkg.com/@iconify-json/lucide/-/lucide-1.2.85.tgz#0074b64f50798da4b89f9f74e4db5a4e56c640b1"
|
||||
resolved "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.2.85.tgz"
|
||||
integrity sha512-VXUWT6KRDiVK4Ty/7Ypu+U0KnSbHzDAOOiSgLLPhU8u3ES5IusP1X7ahZb1iwiVKGWRG6gkKywaRUIZLgYWXyA==
|
||||
dependencies:
|
||||
"@iconify/types" "*"
|
||||
|
||||
"@iconify-json/material-symbols@^1.2.51":
|
||||
version "1.2.51"
|
||||
resolved "https://registry.yarnpkg.com/@iconify-json/material-symbols/-/material-symbols-1.2.51.tgz#270862a21bb65a8632de4943146096b5a58863ae"
|
||||
resolved "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.51.tgz"
|
||||
integrity sha512-GkxlK8ocHi3NVVozaW62jm3qR9fNY3xX2penFtIRvoe1OtNhJ2KD4KRzv8x34pugMOAZYK8sALMcU30gDgCi1A==
|
||||
dependencies:
|
||||
"@iconify/types" "*"
|
||||
@@ -1942,20 +1942,20 @@
|
||||
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
|
||||
|
||||
"@tybys/wasm-util@^0.10.0":
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.0.tgz#2fd3cd754b94b378734ce17058d0507c45c88369"
|
||||
integrity sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414"
|
||||
integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@types/body-scroll-lock@^3.1.2":
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/body-scroll-lock/-/body-scroll-lock-3.1.2.tgz#1ae7857d98180dbe6c3b05abbe7ec1fa67b614e3"
|
||||
resolved "https://registry.npmjs.org/@types/body-scroll-lock/-/body-scroll-lock-3.1.2.tgz"
|
||||
integrity sha512-ELhtuphE/YbhEcpBf/rIV9Tl3/O0A0gpCVD+oYFSS8bWstHFJUgA4nNw1ZakVlRC38XaQEIsBogUZKWIPBvpfQ==
|
||||
|
||||
"@types/crypto-js@^4.2.2":
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea"
|
||||
resolved "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz"
|
||||
integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==
|
||||
|
||||
"@types/debug@^4.1.12":
|
||||
@@ -1999,7 +1999,7 @@
|
||||
|
||||
"@types/linkify-it@^5":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76"
|
||||
resolved "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz"
|
||||
integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==
|
||||
|
||||
"@types/lodash-es@^4.17.12":
|
||||
@@ -2016,14 +2016,14 @@
|
||||
|
||||
"@types/markdown-it-link-attributes@^3.0.5":
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/markdown-it-link-attributes/-/markdown-it-link-attributes-3.0.5.tgz#521179990cd2ced55761d9b8c93e502b679df329"
|
||||
resolved "https://registry.npmjs.org/@types/markdown-it-link-attributes/-/markdown-it-link-attributes-3.0.5.tgz"
|
||||
integrity sha512-VZ2BGN3ywUg7mBD8W6PwR8ChpOxaQSBDbLqPgvNI+uIra3zY2af1eG/3XzWTKjEraTWskMKnZqZd6m1fDF67Bg==
|
||||
dependencies:
|
||||
"@types/markdown-it" "*"
|
||||
|
||||
"@types/markdown-it@*", "@types/markdown-it@^14.1.2":
|
||||
version "14.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61"
|
||||
resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz"
|
||||
integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==
|
||||
dependencies:
|
||||
"@types/linkify-it" "^5"
|
||||
@@ -2031,7 +2031,7 @@
|
||||
|
||||
"@types/mdurl@^2":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
|
||||
resolved "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz"
|
||||
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
|
||||
|
||||
"@types/mousetrap@^1.6.15":
|
||||
@@ -2068,7 +2068,7 @@
|
||||
|
||||
"@types/qrcode@^1.5.6":
|
||||
version "1.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.5.6.tgz#07c33cb9ec0ad88be4636e636e28e54d99b65f42"
|
||||
resolved "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz"
|
||||
integrity sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
@@ -2939,7 +2939,7 @@ body-parser@1.20.3:
|
||||
|
||||
body-scroll-lock@^3.1.5:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz#c1392d9217ed2c3e237fee1e910f6cdd80b7aaec"
|
||||
resolved "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz"
|
||||
integrity sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg==
|
||||
|
||||
boolbase@^1.0.0:
|
||||
@@ -3071,7 +3071,7 @@ camelcase-css@^2.0.1:
|
||||
|
||||
camelcase@^5.0.0:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
|
||||
resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz"
|
||||
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
||||
|
||||
caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702:
|
||||
@@ -3164,7 +3164,7 @@ clean-regexp@^1.0.0:
|
||||
|
||||
cliui@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
|
||||
resolved "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz"
|
||||
integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
@@ -3330,7 +3330,7 @@ cross-spawn@^7.0.6:
|
||||
|
||||
crypto-js@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
|
||||
resolved "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz"
|
||||
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
|
||||
|
||||
crypto-random-string@^2.0.0:
|
||||
@@ -3529,7 +3529,7 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3
|
||||
|
||||
decamelize@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||
resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
|
||||
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
|
||||
|
||||
deep-is@^0.1.3:
|
||||
@@ -3617,7 +3617,7 @@ didyoumean@^1.2.2:
|
||||
|
||||
dijkstrajs@^1.0.1:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23"
|
||||
resolved "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz"
|
||||
integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==
|
||||
|
||||
dir-glob@^3.0.1:
|
||||
@@ -4531,7 +4531,7 @@ gensync@^1.0.0-beta.2:
|
||||
|
||||
get-caller-file@^2.0.1:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
|
||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
|
||||
get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0:
|
||||
@@ -5397,7 +5397,7 @@ lines-and-columns@^1.1.6:
|
||||
|
||||
linkify-it@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421"
|
||||
resolved "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz"
|
||||
integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
|
||||
dependencies:
|
||||
uc.micro "^2.0.0"
|
||||
@@ -5505,12 +5505,12 @@ magic-string@^0.30.11, magic-string@^0.30.17:
|
||||
|
||||
markdown-it-link-attributes@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/markdown-it-link-attributes/-/markdown-it-link-attributes-4.0.1.tgz#25751f2cf74fd91f0a35ba7b3247fa45f2056d88"
|
||||
resolved "https://registry.npmjs.org/markdown-it-link-attributes/-/markdown-it-link-attributes-4.0.1.tgz"
|
||||
integrity sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ==
|
||||
|
||||
markdown-it@^14.1.0:
|
||||
version "14.1.0"
|
||||
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45"
|
||||
resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz"
|
||||
integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
@@ -5552,7 +5552,7 @@ mdn-data@^2.15.0:
|
||||
|
||||
mdurl@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
|
||||
resolved "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz"
|
||||
integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
|
||||
|
||||
media-typer@0.3.0:
|
||||
@@ -6158,7 +6158,7 @@ pluralize@^8.0.0:
|
||||
|
||||
pngjs@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
|
||||
resolved "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz"
|
||||
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
|
||||
|
||||
possible-typed-array-names@^1.0.0:
|
||||
@@ -6315,7 +6315,7 @@ pump@^3.0.0:
|
||||
|
||||
punycode.js@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
|
||||
resolved "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz"
|
||||
integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
|
||||
|
||||
punycode@^2.1.0:
|
||||
@@ -6325,7 +6325,7 @@ punycode@^2.1.0:
|
||||
|
||||
qrcode@^1.5.4:
|
||||
version "1.5.4"
|
||||
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.4.tgz#5cb81d86eb57c675febb08cf007fff963405da88"
|
||||
resolved "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz"
|
||||
integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==
|
||||
dependencies:
|
||||
dijkstrajs "^1.0.1"
|
||||
@@ -6538,7 +6538,7 @@ regjsparser@^0.12.0:
|
||||
|
||||
require-directory@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||
resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz"
|
||||
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
|
||||
|
||||
require-from-string@^2.0.2:
|
||||
@@ -6548,7 +6548,7 @@ require-from-string@^2.0.2:
|
||||
|
||||
require-main-filename@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
|
||||
resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz"
|
||||
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
|
||||
|
||||
requires-port@^1.0.0:
|
||||
@@ -6754,7 +6754,7 @@ serve-static@1.16.2:
|
||||
|
||||
set-blocking@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||
resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz"
|
||||
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
|
||||
|
||||
set-function-length@^1.2.2:
|
||||
@@ -7548,7 +7548,7 @@ typescript@^5, typescript@^5.0.4:
|
||||
|
||||
uc.micro@^2.0.0, uc.micro@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee"
|
||||
resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz"
|
||||
integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
|
||||
|
||||
ufo@^1.5.4, ufo@^1.6.1:
|
||||
@@ -8062,7 +8062,7 @@ which-collection@^1.0.2:
|
||||
|
||||
which-module@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409"
|
||||
resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz"
|
||||
integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
|
||||
|
||||
which-typed-array@^1.1.16, which-typed-array@^1.1.18:
|
||||
@@ -8266,7 +8266,7 @@ workbox-window@7.3.0, workbox-window@^7.3.0:
|
||||
|
||||
wrap-ansi@^6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz"
|
||||
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
@@ -8302,7 +8302,7 @@ xml-name-validator@^4.0.0:
|
||||
|
||||
y18n@^4.0.0:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
|
||||
resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz"
|
||||
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
|
||||
|
||||
yallist@^3.0.2:
|
||||
@@ -8330,7 +8330,7 @@ yaml@^2.0.0, yaml@^2.3.4, yaml@^2.7.0:
|
||||
|
||||
yargs-parser@^18.1.2:
|
||||
version "18.1.3"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
|
||||
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz"
|
||||
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
|
||||
dependencies:
|
||||
camelcase "^5.0.0"
|
||||
@@ -8338,7 +8338,7 @@ yargs-parser@^18.1.2:
|
||||
|
||||
yargs@^15.3.1:
|
||||
version "15.4.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
|
||||
resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz"
|
||||
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
|
||||
dependencies:
|
||||
cliui "^6.0.0"
|
||||
|
||||
Reference in New Issue
Block a user