更新国际化支持

This commit is contained in:
jxxghp
2025-04-28 13:23:51 +08:00
parent 6b49464059
commit 8cf4b612d5
29 changed files with 3175 additions and 17187 deletions
+6 -1
View File
@@ -25,6 +25,7 @@ declare global {
const createPinia: typeof import('pinia')['createPinia'] const createPinia: typeof import('pinia')['createPinia']
const createProjection: typeof import('@vueuse/math')['createProjection'] const createProjection: typeof import('@vueuse/math')['createProjection']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createRef: typeof import('@vueuse/core')['createRef']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
@@ -159,6 +160,7 @@ declare global {
const useCloned: typeof import('@vueuse/core')['useCloned'] const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode'] const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCountdown: typeof import('@vueuse/core')['useCountdown']
const useCounter: typeof import('@vueuse/core')['useCounter'] const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule'] const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar'] const useCssVar: typeof import('@vueuse/core')['useCssVar']
@@ -326,7 +328,7 @@ declare global {
// for type re-export // for type re-export
declare global { declare global {
// @ts-ignore // @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue') import('vue')
} }
@@ -354,6 +356,7 @@ declare module 'vue' {
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']> readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
readonly createProjection: UnwrapRef<typeof import('@vueuse/math')['createProjection']> readonly createProjection: UnwrapRef<typeof import('@vueuse/math')['createProjection']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']> readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createRef: UnwrapRef<typeof import('@vueuse/core')['createRef']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']> readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']> readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']> readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
@@ -488,6 +491,7 @@ declare module 'vue' {
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']> readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']> readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']> readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useCountdown: UnwrapRef<typeof import('@vueuse/core')['useCountdown']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']> readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']> readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']> readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
@@ -527,6 +531,7 @@ declare module 'vue' {
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']> readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']> readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']> readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useId: UnwrapRef<typeof import('vue')['useId']> readonly useId: UnwrapRef<typeof import('vue')['useId']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']> readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']> readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
+1
View File
@@ -2,6 +2,7 @@
// @ts-nocheck // @ts-nocheck
// Generated by unplugin-vue-components // Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {} export {}
/* prettier-ignore */ /* prettier-ignore */
-14609
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -2,6 +2,7 @@
"name": "moviepilot", "name": "moviepilot",
"version": "2.4.3", "version": "2.4.3",
"private": true, "private": true,
"type": "module",
"bin": "dist/service.js", "bin": "dist/service.js",
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite --host",
+1 -1
View File
@@ -1,4 +1,4 @@
module.exports = { export default {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
+8
View File
@@ -12,6 +12,14 @@
*/ */
import { promises as fs } from 'node:fs' import { promises as fs } from 'node:fs'
import { dirname, join } from 'node:path' import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
// Get current directory
const __dirname = dirname(fileURLToPath(import.meta.url))
// Create require function for importing JSON files in ESM
const require = createRequire(import.meta.url)
// Installation: npm install --save-dev @iconify/tools @iconify/utils @iconify/json @iconify/iconify // Installation: npm install --save-dev @iconify/tools @iconify/utils @iconify/json @iconify/iconify
import { import {
+3 -3
View File
@@ -1,17 +1,17 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "CommonJS", "module": "Node16",
"declaration": false, "declaration": false,
"declarationMap": false, "declarationMap": false,
"sourceMap": false, "sourceMap": false,
"composite": false, "composite": false,
"strict": true, "strict": true,
"moduleResolution": "node", "moduleResolution": "node16",
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
}, },
"exclude": [ "exclude": [
"./*.js" "./*.js"
] ]
} }
+1 -1
View File
@@ -1 +1 @@
{"root":["./build-icons.ts"],"version":"5.7.3"} {"root":["./build-icons.ts"],"version":"5.8.3"}
+1 -1
View File
@@ -6,7 +6,7 @@ import api from '@/api'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { getBrowserLocale, setI18nLanguage } from './plugins/i18n' import { getBrowserLocale, setI18nLanguage } from './plugins/i18n'
import { SupportedLocale } from './locales/types' import { SupportedLocale } from '@/types/i18n'
// 国际化 // 国际化
const { t } = useI18n() const { t } = useI18n()
+4
View File
@@ -5,6 +5,10 @@ import FileNavigator from './filebrowser/FileNavigator.vue'
import type { EndPoints, FileItem, StorageConf } from '@/api/types' import type { EndPoints, FileItem, StorageConf } from '@/api/types'
import { storageOptions } from '@/api/constants' import { storageOptions } from '@/api/constants'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
+51 -44
View File
@@ -3,6 +3,10 @@ import type { TransferDirectoryConf } from '@/api/types'
import api from '@/api' import api from '@/api'
import { nextTick } from 'vue' import { nextTick } from 'vue'
import { storageOptions } from '@/api/constants' import { storageOptions } from '@/api/constants'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
@@ -23,11 +27,11 @@ const props = defineProps({
const isCollapsed = ref(true) const isCollapsed = ref(true)
// 类型下拉字典 // 类型下拉字典
const typeItems = [ const typeItems = computed(() => [
{ title: '全部', value: '' }, { title: t('common.all'), value: '' },
{ title: '电影', value: '电影' }, { title: t('media.movie'), value: '电影' },
{ title: '电视剧', value: '电视剧' }, { title: t('media.tv'), value: '电视剧' },
] ])
// 计算资源存储字典(整理方式为下载器时不能为远程存储) // 计算资源存储字典(整理方式为下载器时不能为远程存储)
const resourceStorageOptions = computed(() => { const resourceStorageOptions = computed(() => {
@@ -35,18 +39,18 @@ const resourceStorageOptions = computed(() => {
}) })
// 自动整理方式下拉字典 // 自动整理方式下拉字典
const transferSourceItems = [ const transferSourceItems = computed(() => [
{ title: '不整理', value: '' }, { title: t('directory.noTransfer'), value: '' },
{ title: '下载器监控', value: 'downloader' }, { title: t('directory.downloaderMonitor'), value: 'downloader' },
{ title: '目录监控', value: 'monitor' }, { title: t('directory.directoryMonitor'), value: 'monitor' },
{ title: '手动整理', value: 'manual' }, { title: t('directory.manualTransfer'), value: 'manual' },
] ])
// 监控模式下拉字典 // 监控模式下拉字典
const MonitorModeItems = [ const MonitorModeItems = computed(() => [
{ title: '性能模式', value: 'fast' }, { title: t('directory.performanceMode'), value: 'fast' },
{ title: '兼容模式', value: 'compatibility' }, { title: t('directory.compatibilityMode'), value: 'compatibility' },
] ])
// 整理方式下拉字典 // 整理方式下拉字典
const transferTypeItems = ref<{ title: string; value: string }[]>([]) const transferTypeItems = ref<{ title: string; value: string }[]>([])
@@ -103,23 +107,23 @@ async function loadTransferTypeItems() {
// 整理方式无数据提示 // 整理方式无数据提示
const computedNoDataText = computed(() => { const computedNoDataText = computed(() => {
if (!props.directory.library_storage && !props.directory.storage) { if (!props.directory.library_storage && !props.directory.storage) {
return '请选择储存' return t('directory.pleaseSelectStorage')
} else if (!props.directory.library_storage) { } else if (!props.directory.library_storage) {
return '请选择媒体库储存' return t('directory.pleaseSelectLibraryStorage')
} else if (!props.directory.storage) { } else if (!props.directory.storage) {
return '请选择下载器储存' return t('directory.pleaseSelectDownloadStorage')
} else { } else {
return '选择的存储类型没有支持的整理方式' return t('directory.noSupportedTransferType')
} }
}) })
// 覆盖模式下拉字典 // 覆盖模式下拉字典
const overwriteModeItems = [ const overwriteModeItems = computed(() => [
{ title: '从不', value: 'never' }, { title: t('directory.never'), value: 'never' },
{ title: '总是', value: 'always' }, { title: t('directory.always'), value: 'always' },
{ title: '按文件大小', value: 'size' }, { title: t('directory.byFileSize'), value: 'size' },
{ title: '仅保留最新版本', value: 'latest' }, { title: t('directory.keepLatestOnly'), value: 'latest' },
] ])
// 定义触发的自定义事件 // 定义触发的自定义事件
const emit = defineEmits(['close', 'changed', 'update:modelValue']) const emit = defineEmits(['close', 'changed', 'update:modelValue'])
@@ -131,7 +135,7 @@ function onClose() {
// 根据选中的媒体类型,获取对应的媒体类别 // 根据选中的媒体类型,获取对应的媒体类别
const getCategories = computed(() => { const getCategories = computed(() => {
const default_value = [{ title: '全部', value: '' }] const default_value = [{ title: t('common.all'), value: '' }]
if (!props.categories || !props.categories[props.directory?.media_type ?? '']) return default_value if (!props.categories || !props.categories[props.directory?.media_type ?? '']) return default_value
return default_value.concat(props.categories[props.directory.media_type ?? '']) return default_value.concat(props.categories[props.directory.media_type ?? ''])
}) })
@@ -180,7 +184,7 @@ watch(
<VTextField <VTextField
v-model="props.directory.name" v-model="props.directory.name"
variant="underlined" variant="underlined"
label="别名" :label="t('directory.alias')"
class="me-20 text-high-emphasis font-weight-bold" class="me-20 text-high-emphasis font-weight-bold"
/> />
<span class="absolute top-3 right-12"> <span class="absolute top-3 right-12">
@@ -197,7 +201,7 @@ watch(
v-model="props.directory.media_type" v-model="props.directory.media_type"
variant="underlined" variant="underlined"
:items="typeItems" :items="typeItems"
label="媒体类型" :label="t('directory.mediaType')"
@update:modelValue="props.directory.media_category = ''" @update:modelValue="props.directory.media_category = ''"
/> />
</VCol> </VCol>
@@ -206,7 +210,7 @@ watch(
v-model="props.directory.media_category" v-model="props.directory.media_category"
variant="underlined" variant="underlined"
:items="getCategories" :items="getCategories"
label="媒体类别" :label="t('directory.mediaCategory')"
/> />
</VCol> </VCol>
<VCol cols="4"> <VCol cols="4">
@@ -214,7 +218,7 @@ watch(
v-model="props.directory.storage" v-model="props.directory.storage"
variant="underlined" variant="underlined"
:items="resourceStorageOptions" :items="resourceStorageOptions"
label="资源存储" :label="t('directory.resourceStorage')"
/> />
</VCol> </VCol>
<VCol cols="8"> <VCol cols="8">
@@ -222,14 +226,17 @@ watch(
v-model="props.directory.download_path" v-model="props.directory.download_path"
:storage="props.directory.storage" :storage="props.directory.storage"
variant="underlined" variant="underlined"
label="资源目录" :label="t('directory.resourceDirectory')"
/> />
</VCol> </VCol>
<VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''"> <VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''">
<VSwitch v-model="props.directory.download_type_folder" label="按类型分类"></VSwitch> <VSwitch v-model="props.directory.download_type_folder" :label="t('directory.sortByType')"></VSwitch>
</VCol> </VCol>
<VCol cols="6" v-if="!props.directory.media_category || props.directory.media_category === ''"> <VCol cols="6" v-if="!props.directory.media_category || props.directory.media_category === ''">
<VSwitch v-model="props.directory.download_category_folder" label="按类别分类"></VSwitch> <VSwitch
v-model="props.directory.download_category_folder"
:label="t('directory.sortByCategory')"
></VSwitch>
</VCol> </VCol>
</VRow> </VRow>
<VDivider v-if="$props.directory.monitor_type" class="my-3 bg-primary" /> <VDivider v-if="$props.directory.monitor_type" class="my-3 bg-primary" />
@@ -239,7 +246,7 @@ watch(
v-model="props.directory.monitor_type" v-model="props.directory.monitor_type"
variant="underlined" variant="underlined"
:items="transferSourceItems" :items="transferSourceItems"
label="自动整理" :label="t('directory.autoTransfer')"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -249,7 +256,7 @@ watch(
v-model="props.directory.monitor_mode" v-model="props.directory.monitor_mode"
variant="underlined" variant="underlined"
:items="MonitorModeItems" :items="MonitorModeItems"
label="监控模式" :label="t('directory.monitorMode')"
/> />
</VCol> </VCol>
<VCol cols="4"> <VCol cols="4">
@@ -257,7 +264,7 @@ watch(
v-model="props.directory.library_storage" v-model="props.directory.library_storage"
variant="underlined" variant="underlined"
:items="storageOptions" :items="storageOptions"
label="媒体库存储" :label="t('directory.libraryStorage')"
/> />
</VCol> </VCol>
<VCol cols="8"> <VCol cols="8">
@@ -265,7 +272,7 @@ watch(
v-model="props.directory.library_path" v-model="props.directory.library_path"
:storage="props.directory.library_storage" :storage="props.directory.library_storage"
variant="underlined" variant="underlined"
label="媒体库目录" :label="t('directory.libraryDirectory')"
/> />
</VCol> </VCol>
<VCol cols="4"> <VCol cols="4">
@@ -273,7 +280,7 @@ watch(
v-model="props.directory.transfer_type" v-model="props.directory.transfer_type"
variant="underlined" variant="underlined"
:items="transferTypeItems" :items="transferTypeItems"
label="整理方式" :label="t('directory.transferType')"
:no-data-text="computedNoDataText" :no-data-text="computedNoDataText"
/> />
</VCol> </VCol>
@@ -282,23 +289,23 @@ watch(
v-model="props.directory.overwrite_mode" v-model="props.directory.overwrite_mode"
variant="underlined" variant="underlined"
:items="overwriteModeItems" :items="overwriteModeItems"
label="覆盖模式" :label="t('directory.overwriteMode')"
/> />
</VCol> </VCol>
<VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''"> <VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''">
<VSwitch v-model="props.directory.library_type_folder" label="按类型分类"></VSwitch> <VSwitch v-model="props.directory.library_type_folder" :label="t('directory.sortByType')"></VSwitch>
</VCol> </VCol>
<VCol cols="6" v-if="!props.directory.media_category || props.directory.media_category === ''"> <VCol cols="6" v-if="!props.directory.media_category || props.directory.media_category === ''">
<VSwitch v-model="props.directory.library_category_folder" label="按类别分类"></VSwitch> <VSwitch v-model="props.directory.library_category_folder" :label="t('directory.sortByCategory')"></VSwitch>
</VCol> </VCol>
<VCol cols="6"> <VCol cols="6">
<VSwitch v-model="props.directory.renaming" label="智能重命名"></VSwitch> <VSwitch v-model="props.directory.renaming" :label="t('directory.smartRename')"></VSwitch>
</VCol> </VCol>
<VCol cols="6"> <VCol cols="6">
<VSwitch v-model="props.directory.scraping" label="刮削元数据"></VSwitch> <VSwitch v-model="props.directory.scraping" :label="t('directory.scrapingMetadata')"></VSwitch>
</VCol> </VCol>
<VCol cols="6"> <VCol cols="6">
<VSwitch v-model="props.directory.notify" label="发送通知"></VSwitch> <VSwitch v-model="props.directory.notify" :label="t('directory.sendNotification')"></VSwitch>
</VCol> </VCol>
</VRow> </VRow>
</VForm> </VForm>
+5 -5
View File
@@ -118,14 +118,14 @@ function getMediaId() {
// 角标颜色 // 角标颜色
function getChipColor(type: string) { function getChipColor(type: string) {
if (type === t('media.movie')) return 'border-blue-500 bg-blue-600' if (type === '电影') return 'border-blue-500 bg-blue-600'
else if (type === t('media.tv')) return ' bg-indigo-500 border-indigo-600' else if (type === '电视剧') return ' bg-indigo-500 border-indigo-600'
else return 'border-purple-600 bg-purple-600' else return 'border-purple-600 bg-purple-600'
} }
// 添加订阅处理 // 添加订阅处理
async function handleAddSubscribe() { async function handleAddSubscribe() {
if (props.media?.type === t('media.tv')) { if (props.media?.type === '电视剧') {
// 弹出季选择列表,支持多选 // 弹出季选择列表,支持多选
seasonsSelected.value = [] seasonsSelected.value = []
subscribeSeasonDialog.value = true subscribeSeasonDialog.value = true
@@ -141,7 +141,7 @@ async function addSubscribe(season: number = 0, best_version: number = 0) {
startNProgress() startNProgress()
try { try {
// 是否洗版 // 是否洗版
if (!best_version && props.media?.type == t('media.movie')) best_version = isExists.value ? 1 : 0 if (!best_version && props.media?.type == '电影') best_version = isExists.value ? 1 : 0
// 请求API // 请求API
const result: { [key: string]: any } = await api.post('subscribe/', { const result: { [key: string]: any } = await api.post('subscribe/', {
name: props.media?.title, name: props.media?.title,
@@ -279,7 +279,7 @@ async function queryDefaultSubscribeConfig() {
if (!userStore.superUser) return false if (!userStore.superUser) return false
try { try {
let subscribe_config_url = '' let subscribe_config_url = ''
if (props.media?.type === t('media.movie')) subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig' if (props.media?.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
else subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig' else subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
const result: { [key: string]: any } = await api.get(subscribe_config_url) const result: { [key: string]: any } = await api.get(subscribe_config_url)
if (result.data?.value) return result.data.value.show_edit_dialog if (result.data?.value) return result.data.value.show_edit_dialog
+24 -14
View File
@@ -10,6 +10,10 @@ import api from '@/api'
import ProgressDialog from '../dialog/ProgressDialog.vue' import ProgressDialog from '../dialog/ProgressDialog.vue'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import MediaInfoDialog from '../dialog/MediaInfoDialog.vue' import MediaInfoDialog from '../dialog/MediaInfoDialog.vue'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 显示器宽度 // 显示器宽度
const display = useDisplay() const display = useDisplay()
@@ -687,25 +691,31 @@ onMounted(() => {
</VCard> </VCard>
<!-- 重命名弹窗 --> <!-- 重命名弹窗 -->
<VDialog v-if="renamePopper" v-model="renamePopper" max-width="35rem"> <VDialog v-if="renamePopper" v-model="renamePopper" max-width="35rem">
<VCard title="重命名"> <VCard :title="t('file.rename')">
<VDialogCloseBtn @click="renamePopper = false" /> <VDialogCloseBtn @click="renamePopper = false" />
<VDivider /> <VDivider />
<VCardText> <VCardText>
<VRow> <div class="mb-3">
<VCol cols="12"> <span>{{ t('file.currentName') }}: {{ currentItem?.name }}</span>
<VTextField v-model="newName" label="新名称" :loading="renameLoading" /> </div>
</VCol> <VTextField v-model="newName" :label="t('file.newName')" />
<VCol cols="12" v-if="currentItem && currentItem.type == 'dir'"> <VCheckbox
<VSwitch v-model="renameAll" label="自动重命名目录内所有媒体文件" /> v-if="false && currentItem?.type == 'dir'"
</VCol> v-model="renameAll"
</VRow> :label="t('file.includeSubfolders')"
></VCheckbox>
</VCardText> </VCardText>
<VCardActions> <VCardActions>
<VBtn color="success" variant="elevated" @click="get_recommend_name" prepend-icon="mdi-magic" class="px-5 me-3"> <div class="flex-grow-1" />
自动识别名称 <VBtn
</VBtn> :disabled="!newName"
<VBtn :disabled="!newName" variant="elevated" @click="rename" prepend-icon="mdi-check" class="px-5 me-3"> variant="elevated"
确定 :loading="renameLoading"
@click="rename"
prepend-icon="mdi-check"
class="px-5 me-3"
>
{{ t('common.save') }}
</VBtn> </VBtn>
</VCardActions> </VCardActions>
</VCard> </VCard>
+5 -1
View File
@@ -3,6 +3,10 @@ import type { PropType } from 'vue'
import type { FileItem } from '@/api/types' import type { FileItem } from '@/api/types'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import type { AxiosRequestConfig } from 'axios' import type { AxiosRequestConfig } from 'axios'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 显示器宽度 // 显示器宽度
const display = useDisplay() const display = useDisplay()
@@ -276,7 +280,7 @@ function getIndentLevel(path: string, ancestorPath: string) {
> >
<div class="folder-content"> <div class="folder-content">
<VIcon icon="mdi-home" class="me-2" color="primary" /> <VIcon icon="mdi-home" class="me-2" color="primary" />
<span>根目录</span> <span>{{ t('file.rootDirectory') }}</span>
</div> </div>
</div> </div>
+7 -3
View File
@@ -2,6 +2,10 @@
import type { AxiosRequestConfig } from 'axios' import type { AxiosRequestConfig } from 'axios'
import type { EndPoints, FileItem } from '@/api/types' import type { EndPoints, FileItem } from '@/api/types'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 显示器宽度 // 显示器宽度
const display = useDisplay() const display = useDisplay()
@@ -167,16 +171,16 @@ const sortIcon = computed(() => {
<VIcon v-bind="props" icon="mdi-folder-plus-outline" /> <VIcon v-bind="props" icon="mdi-folder-plus-outline" />
</IconBtn> </IconBtn>
</template> </template>
<VCard title="新建文件夹"> <VCard :title="t('file.newFolder')">
<VDialogCloseBtn @click="newFolderPopper = false" /> <VDialogCloseBtn @click="newFolderPopper = false" />
<VDivider /> <VDivider />
<VCardText> <VCardText>
<VTextField v-model="newFolderName" label="名称" /> <VTextField v-model="newFolderName" :label="t('common.name')" />
</VCardText> </VCardText>
<VCardActions> <VCardActions>
<div class="flex-grow-1" /> <div class="flex-grow-1" />
<VBtn :disabled="!newFolderName" variant="elevated" @click="mkdir" prepend-icon="mdi-check" class="px-5 me-3"> <VBtn :disabled="!newFolderName" variant="elevated" @click="mkdir" prepend-icon="mdi-check" class="px-5 me-3">
新建 {{ t('common.create') }}
</VBtn> </VBtn>
</VCardActions> </VCardActions>
</VCard> </VCard>
+294 -173
View File
@@ -14,14 +14,18 @@ export default {
inputMessage: 'Enter message or command', inputMessage: 'Enter message or command',
send: 'Send', send: 'Send',
noData: 'No Data', noData: 'No Data',
noContent: 'No Content Found', noContent: 'No relevant content found',
all: 'All', all: 'All',
default: 'Default', default: 'Default',
name: 'Name',
create: 'Create',
saving: 'Saving',
reset: 'Reset',
}, },
theme: { theme: {
light: 'Light', light: 'Light',
dark: 'Dark', dark: 'Dark',
auto: 'Auto', auto: 'Follow System',
transparent: 'Transparent', transparent: 'Transparent',
purple: 'Purple', purple: 'Purple',
custom: 'Custom Theme', custom: 'Custom Theme',
@@ -36,22 +40,22 @@ export default {
selectLanguage: 'Select Language', selectLanguage: 'Select Language',
logout: 'Logout', logout: 'Logout',
restarting: 'Restarting...', restarting: 'Restarting...',
confirmRestart: 'Confirm Restart?', confirmRestart: 'Confirm restart system?',
restartTip: 'After restarting, you will be logged out and need to log in again.', restartTip: 'After restart, you will be logged out and need to log in again.',
}, },
login: { login: {
wallpapers: 'wallpapers', wallpapers: 'Wallpapers',
username: 'Username', username: 'Username',
password: 'Password', password: 'Password',
otpCode: 'OTP Code', otpCode: 'Two-Factor Code',
stayLoggedIn: 'Stay Logged In', stayLoggedIn: 'Stay Logged In',
login: 'Login', login: 'Login',
networkError: 'Login failed, please check your network connection!', networkError: 'Login failed, please check your network connection!',
authFailure: 'Login failed, please check your username, password or OTP code!', authFailure: 'Login failed, please check your username, password or two-factor authentication!',
permissionDenied: 'Login failed, you do not have permission to access!', permissionDenied: 'Login failed, you do not have permission to access!',
serverError: 'Login failed, server error!', serverError: 'Login failed, server error!',
loginFailed: 'Login Failed', loginFailed: 'Login Failed',
checkCredentials: 'Please check your username, password or OTP code!', checkCredentials: 'Please check your username, password or two-factor authentication code!',
}, },
menu: { menu: {
start: 'Start', start: 'Start',
@@ -71,7 +75,7 @@ export default {
movieSubscribe: 'Movie Subscription', movieSubscribe: 'Movie Subscription',
tvSubscribe: 'TV Subscription', tvSubscribe: 'TV Subscription',
history: 'History', history: 'History',
transfer: 'Transfer', transfer: 'Organize',
rename: 'Rename', rename: 'Rename',
statistic: 'Statistics', statistic: 'Statistics',
setting: 'Settings', setting: 'Settings',
@@ -84,11 +88,11 @@ export default {
workflow: 'Workflow', workflow: 'Workflow',
calendar: 'Calendar', calendar: 'Calendar',
downloadManager: 'Download Manager', downloadManager: 'Download Manager',
mediaOrganize: 'Media Organizer', mediaOrganize: 'Media Organize',
fileManager: 'File Manager', fileManager: 'File Manager',
pluginManager: 'Plugins', pluginManager: 'Plugins',
siteManager: 'Site Manager', siteManager: 'Site Management',
userManager: 'User Manager', userManager: 'User Management',
settings: 'Settings', settings: 'Settings',
}, },
settingTabs: { settingTabs: {
@@ -98,7 +102,7 @@ export default {
}, },
directory: { directory: {
title: 'Storage & Directories', title: 'Storage & Directories',
description: 'Download directories, media library directories, organization, metadata scraping', description: 'Download directory, media library directory, organization, scraping',
}, },
site: { site: {
title: 'Sites', title: 'Sites',
@@ -110,15 +114,15 @@ export default {
}, },
search: { search: {
title: 'Search & Download', title: 'Search & Download',
description: 'Search data sources (TheMovieDb, Douban, Bangumi), download task labels, search sites', description: 'Search data sources (TheMovieDb, Douban, Bangumi), download task tags, search sites',
}, },
subscribe: { subscribe: {
title: 'Subscription', title: 'Subscription',
description: 'Subscription sites, subscription modes, subscription rules, upgrade rules', description: 'Subscription sites, subscription mode, subscription rules, version upgrade rules',
}, },
scheduler: { scheduler: {
title: 'Services', title: 'Services',
description: 'Scheduled tasks', description: 'Scheduled jobs',
}, },
notification: { notification: {
title: 'Notifications', title: 'Notifications',
@@ -126,7 +130,7 @@ export default {
}, },
words: { words: {
title: 'Word Lists', title: 'Word Lists',
description: 'Custom recognition words, custom groups, custom placeholders, file organization filter words', description: 'Custom recognition words, custom production/subtitle groups, custom placeholders, file organization block words',
}, },
about: { about: {
title: 'About', title: 'About',
@@ -136,12 +140,12 @@ export default {
subscribeTabs: { subscribeTabs: {
movie: { movie: {
mysub: 'My Subscriptions', mysub: 'My Subscriptions',
popular: 'Popular', popular: 'Popular Subscriptions',
}, },
tv: { tv: {
mysub: 'My Subscriptions', mysub: 'My Subscriptions',
popular: 'Popular', popular: 'Popular Subscriptions',
share: 'Shared', share: 'Subscription Shares',
}, },
}, },
pluginTabs: { pluginTabs: {
@@ -155,12 +159,26 @@ export default {
}, },
user: { user: {
admin: 'Administrator', admin: 'Administrator',
normalUser: 'Normal User', normalUser: 'Regular User',
profile: 'Profile', profile: 'Profile',
systemSettings: 'System Settings', systemSettings: 'System Settings',
siteAuth: 'User Authentication', siteAuth: 'User Authentication',
helpDocs: 'Help Docs', helpDocs: 'Help Documents',
restart: 'Restart', restart: 'Restart',
management: 'User Management',
noUsers: 'No Users',
clickToAddUser: 'Click Add User card to add users',
addUser: 'Add User',
editUser: 'Edit User',
username: 'Username',
password: 'Password',
confirmPassword: 'Confirm Password',
role: 'Role',
email: 'Email',
enabled: 'Enabled',
disabled: 'Disabled',
status: 'Status',
operations: 'Operations',
}, },
nav: { nav: {
more: 'More', more: 'More',
@@ -178,7 +196,7 @@ export default {
}, },
rule: { rule: {
title: 'Rules', title: 'Rules',
subtitle: 'Rule Test', subtitle: 'Rule Testing',
}, },
log: { log: {
title: 'Logs', title: 'Logs',
@@ -186,7 +204,7 @@ export default {
}, },
network: { network: {
title: 'Network', title: 'Network',
subtitle: 'Speed & Connectivity Test', subtitle: 'Network Speed and Connectivity Test',
}, },
system: { system: {
title: 'System', title: 'System',
@@ -202,7 +220,7 @@ export default {
clickToAdd: 'Click to Add', clickToAdd: 'Click to Add',
dragToCanvas: 'Drag to Canvas', dragToCanvas: 'Drag to Canvas',
tapComponentHint: 'Tap component to add to canvas', tapComponentHint: 'Tap component to add to canvas',
dragComponentHint: 'Drag components to canvas', dragComponentHint: 'Drag component to canvas',
}, },
dashboard: { dashboard: {
storage: 'Storage Space', storage: 'Storage Space',
@@ -224,9 +242,9 @@ export default {
tv: 'TV Show', tv: 'TV Show',
}, },
subscribe: { subscribe: {
normalSub: 'Subscription', normalSub: 'Subscribe',
versionSub: 'Version Upgrade', versionSub: 'Version Upgrade Subscribe',
addSuccess: '{name} added successfully!', addSuccess: 'Added {name} successfully!',
addFailed: 'Failed to add {name}: {message}!', addFailed: 'Failed to add {name}: {message}!',
cancelSuccess: 'Subscription cancelled!', cancelSuccess: 'Subscription cancelled!',
cancelFailed: 'Failed to cancel subscription: {message}!', cancelFailed: 'Failed to cancel subscription: {message}!',
@@ -234,18 +252,16 @@ export default {
name: 'Name', name: 'Name',
searchShares: 'Search Subscription Shares', searchShares: 'Search Subscription Shares',
keyword: 'Keyword', keyword: 'Keyword',
noShareData: noShareData: 'No shared subscription data received, data sharing not enabled or server cannot connect.',
'No shared subscription data available, data sharing is not enabled or the server cannot be connected.', noPopularData: 'No popular subscription data received, data sharing not enabled or server cannot connect.',
noPopularData: noFilterData: 'No related content found with current filters, please change filter conditions.',
'No popular subscription data available, data sharing is not enabled or the server cannot be connected.',
noFilterData: 'No matching content found. Please change the filter criteria.',
noSubscribeData: 'Please search to add movie or TV show subscriptions.', noSubscribeData: 'Please search to add movie or TV show subscriptions.',
sharer: 'Shared by', sharer: 'Sharer',
follow: 'Follow', follow: 'Follow',
unfollow: 'Unfollow', unfollow: 'Unfollow',
recognitionWords: 'Recognition Words', recognitionWords: 'Recognition Words',
cancelShare: 'Cancel Share', cancelShare: 'Cancel Share',
usageCount: 'Used {count} times', usageCount: '{count} Uses',
}, },
recommend: { recommend: {
all: 'All', all: 'All',
@@ -255,7 +271,7 @@ export default {
categoryRankings: 'Rankings', categoryRankings: 'Rankings',
trendingNow: 'Trending Now', trendingNow: 'Trending Now',
nowShowing: 'Now Showing', nowShowing: 'Now Showing',
bangumiDaily: 'Bangumi Daily', bangumiDaily: 'Bangumi Daily Release',
tmdbHotMovies: 'TMDB Hot Movies', tmdbHotMovies: 'TMDB Hot Movies',
tmdbHotTVShows: 'TMDB Hot TV Shows', tmdbHotTVShows: 'TMDB Hot TV Shows',
doubanHotMovies: 'Douban Hot Movies', doubanHotMovies: 'Douban Hot Movies',
@@ -263,13 +279,13 @@ export default {
doubanHotAnime: 'Douban Hot Anime', doubanHotAnime: 'Douban Hot Anime',
doubanNewMovies: 'Douban New Movies', doubanNewMovies: 'Douban New Movies',
doubanNewTVShows: 'Douban New TV Shows', doubanNewTVShows: 'Douban New TV Shows',
doubanTop250: 'Douban Movie TOP250', doubanTop250: 'Douban Top 250 Movies',
doubanChineseTVRankings: 'Douban Chinese TV Rankings', doubanChineseTVRankings: 'Douban Chinese TV Rankings',
doubanGlobalTVRankings: 'Douban Global TV Rankings', doubanGlobalTVRankings: 'Douban Global TV Rankings',
noCategoryContent: 'No content to display in this category', noCategoryContent: 'No content to display in current category',
configureContent: 'Configure Content', configureContent: 'Configure Display Content',
customizeContent: 'Customize Content', customizeContent: 'Customize Content',
selectContentToDisplay: 'Select the content you want to display', selectContentToDisplay: 'Select content you want to display on the page',
selectAll: 'Select All', selectAll: 'Select All',
selectNone: 'Select None', selectNone: 'Select None',
}, },
@@ -278,8 +294,8 @@ export default {
dragToReorder: 'Drag to reorder tabs', dragToReorder: 'Drag to reorder tabs',
}, },
downloading: { downloading: {
noDownloader: 'No Downloaders', noDownloader: 'No Downloader',
configureDownloader: 'Please configure and enable downloaders in settings first.', configureDownloader: 'Please configure and enable a downloader in settings first.',
}, },
resource: { resource: {
searchResults: 'Resource Search Results', searchResults: 'Resource Search Results',
@@ -301,8 +317,8 @@ export default {
}, },
notFound: { notFound: {
title: 'Page Not Found ⚠️', title: 'Page Not Found ⚠️',
description: 'The page you are trying to access does not exist. Please check the URL.', description: 'The page you tried to access does not exist. Please check if the address is correct.',
backButton: 'Back', backButton: 'Go Back',
}, },
torrent: { torrent: {
sortDefault: 'Default', sortDefault: 'Default',
@@ -310,9 +326,9 @@ export default {
sortSize: 'Size', sortSize: 'Size',
sortSeeder: 'Seeders', sortSeeder: 'Seeders',
filterSite: 'Site', filterSite: 'Site',
filterSeason: 'Season', filterSeason: 'Season/Episode',
filterFreeState: 'Promotion', filterFreeState: 'Promotion Status',
filterVideoCode: 'Video Code', filterVideoCode: 'Video Codec',
filterEdition: 'Quality', filterEdition: 'Quality',
filterResolution: 'Resolution', filterResolution: 'Resolution',
filterReleaseGroup: 'Release Group', filterReleaseGroup: 'Release Group',
@@ -325,15 +341,15 @@ export default {
episode: 'Episode {number}', episode: 'Episode {number}',
}, },
storage: { storage: {
usedPercent: 'Used {percent}%', usedPercent: '{percent}% Used',
}, },
site: { site: {
noSites: 'No Sites', noSites: 'No Sites',
sitesWillBeShownHere: 'Added and supported sites will be displayed here.', sitesWillBeShownHere: 'Added and supported sites will be shown here.',
}, },
message: { message: {
loadMore: 'Load More', loadMore: 'Load More',
noMoreData: 'No More Data', noMoreData: 'No more data',
}, },
logging: { logging: {
level: 'Level', level: 'Level',
@@ -833,7 +849,7 @@ export default {
otp: 'Enable Two-Factor Authentication', otp: 'Enable Two-Factor Authentication',
avatar: 'Avatar', avatar: 'Avatar',
uploadAvatar: 'Upload Avatar', uploadAvatar: 'Upload Avatar',
resetDefaultAvatar: 'Reset to Default Avatar', resetDefaultAvatar: 'Reset Default Avatar',
restoreCurrentAvatar: 'Restore Current Avatar', restoreCurrentAvatar: 'Restore Current Avatar',
notifications: 'Notifications', notifications: 'Notifications',
wechat: 'WeChat UserID', wechat: 'WeChat UserID',
@@ -846,28 +862,28 @@ export default {
updatingUser: 'Updating user [{name}], please wait', updatingUser: 'Updating user [{name}], please wait',
usernameRequired: 'Username cannot be empty', usernameRequired: 'Username cannot be empty',
usernameExists: 'Username already exists', usernameExists: 'Username already exists',
passwordMismatch: 'Passwords do not match', passwordMismatch: 'The two passwords do not match',
userCreated: 'User [{name}] created successfully', userCreated: 'User [{name}] created successfully',
userCreateFailed: 'Failed to create user: {message}', userCreateFailed: 'Failed to create user: {message}',
userUpdateSuccess: 'User [{name}] updated successfully', userUpdateSuccess: 'User [{name}] updated successfully',
userUpdateFailed: 'Failed to update user: {message}', userUpdateFailed: 'Failed to update user: {message}',
userDeleteSuccess: 'User [{name}] deleted successfully', userDeleteSuccess: 'User [{name}] deleted successfully',
userDeleteFailed: 'Failed to delete user: {message}', userDeleteFailed: 'Failed to delete user: {message}',
invalidFile: 'The uploaded file does not meet requirements, please select another avatar', invalidFile: 'The uploaded file does not meet the requirements, please choose a new avatar',
fileSizeLimit: 'File size cannot exceed 800KB', fileSizeLimit: 'File size must not exceed 800KB',
avatarUploadSuccess: 'New avatar uploaded successfully, will take effect after saving!', avatarUploadSuccess: 'New avatar uploaded successfully, will take effect after saving!',
resetAvatarSuccess: 'Reset to default avatar, will take effect after saving!', resetAvatarSuccess: 'Reset to default avatar, will take effect after saving!',
restoreAvatarSuccess: 'Current avatar restored!', restoreAvatarSuccess: 'Restored current avatar!',
deleteConfirm: 'Confirm delete user [{name}]?', deleteConfirm: 'Confirm delete user [{name}]?',
saveUserInfo: 'Save User Information', saveUserInfo: 'Save User Information',
cannotDeleteCurrentUser: 'Cannot delete the currently logged-in user', cannotDeleteCurrentUser: 'Cannot delete current logged-in user',
deleteUser: 'Delete User', deleteUser: 'Delete User',
}, },
searchBar: { searchBar: {
search: 'Search', search: 'Search',
searchPlaceholder: 'Search functions, subscriptions, settings...', searchPlaceholder: 'Search features, subscriptions, settings...',
recentSearches: 'Recent Searches', recentSearches: 'Recent Searches',
noRecentSearches: 'No recent searches', noRecentSearches: 'No recent search history',
functions: 'Functions', functions: 'Functions',
noFunctionsFound: 'No matching functions', noFunctionsFound: 'No matching functions',
plugins: 'Plugins', plugins: 'Plugins',
@@ -877,13 +893,13 @@ export default {
searchSites: 'Search Sites', searchSites: 'Search Sites',
selectSites: 'Select Sites', selectSites: 'Select Sites',
collections: 'Collections', collections: 'Collections',
collectionSearch: 'related collections', collectionSearch: 'Related series works',
actorSearch: 'related actors, directors, etc.', actorSearch: 'Related actors, directors, etc.',
historySearch: 'related history records', historySearch: 'Related history records',
siteResources: 'Site Resources', siteResources: 'Site Resources',
searchInSites: 'Search for torrents in sites', searchInSites: 'Search for torrent resources in sites',
relatedResources: 'related resources', relatedResources: 'Related Resources',
searchTip: 'Search for movies, TV shows, actors, resources, etc.', searchTip: 'You can search for movies, TV shows, actors, resources, etc.',
}, },
searchSite: { searchSite: {
selectSites: 'Select Sites', selectSites: 'Select Sites',
@@ -903,7 +919,7 @@ export default {
downloader: 'Downloader (Default)', downloader: 'Downloader (Default)',
saveDirectory: 'Save Directory (Auto)', saveDirectory: 'Save Directory (Auto)',
defaultPlaceholder: 'Leave empty for default', defaultPlaceholder: 'Leave empty for default',
autoPlaceholder: 'Leave empty for auto match', autoPlaceholder: 'Leave empty for auto-match',
downloading: 'Downloading...', downloading: 'Downloading...',
startDownload: 'Start Download', startDownload: 'Start Download',
downloadSuccess: '{site} {title} downloaded successfully!', downloadSuccess: '{site} {title} downloaded successfully!',
@@ -914,41 +930,39 @@ export default {
season: 'Season {number}', season: 'Season {number}',
title: 'Title', title: 'Title',
description: 'Description', description: 'Description',
descriptionHint: descriptionHint: 'Add a description about this subscription. Search terms, recognition words, etc. will be included in the share by default',
'Fill in the description of the subscription. Search terms and identification terms in the subscription will be included in the share by default',
shareUser: 'Share User', shareUser: 'Share User',
shareUserHint: 'Nickname of the sharer', shareUserHint: 'Sharer\'s nickname',
confirmShare: 'Confirm Share', confirmShare: 'Confirm Share',
shareSuccess: '{name} shared successfully!', shareSuccess: '{name} shared successfully!',
shareFailed: '{name} share failed: {message}!', shareFailed: '{name} share failed: {message}!',
}, },
u115Auth: { u115Auth: {
loginTitle: '115 Cloud Login', loginTitle: '115 Cloud Login',
scanQrCode: 'Please scan the QR code with WeChat or 115 client', scanQrCode: 'Please scan with WeChat or 115 client',
scanned: 'QR code scanned, please confirm login', scanned: 'Scanned, please confirm login',
complete: 'Complete', complete: 'Complete',
}, },
aliyunAuth: { aliyunAuth: {
loginTitle: 'Aliyun Drive Login', loginTitle: 'Aliyun Drive Login',
scanQrCode: 'Please scan with Aliyun Drive App', scanQrCode: 'Please scan with Aliyun Drive App',
scanned: 'QR code scanned', scanned: 'Scanned',
complete: 'Complete', complete: 'Complete',
}, },
rcloneConfig: { rcloneConfig: {
title: 'RClone Configuration', title: 'RClone Configuration',
filePath: 'Rclone Config File Path', filePath: 'rclone config file path',
fileContent: 'Rclone Config File Content', fileContent: 'rclone config file content',
defaultContent: defaultContent: '# Please fill in your rclone config file content here \n# Please refer to https://rclone.org/docs/ \n# Storage node name must be: MP',
'# Please fill in the rclone configuration file content here \n# Please refer to https://rclone.org/docs/ \n# Storage node name must be: MP',
complete: 'Complete', complete: 'Complete',
}, },
alistConfig: { alistConfig: {
title: 'Alist Configuration', title: 'Alist Configuration',
serverUrl: 'Alist Server URL', serverUrl: 'Alist server address',
username: 'Username', username: 'Username',
password: 'Password', password: 'Password',
tokenUrl: 'Token URL', tokenUrl: 'Token acquisition address',
loginType: 'Login Type', loginType: 'Login method',
loginTypeOptions: { loginTypeOptions: {
guest: 'Guest', guest: 'Guest',
username: 'Username & Password', username: 'Username & Password',
@@ -967,17 +981,17 @@ export default {
schedule: 'Schedule', schedule: 'Schedule',
cronExpr: 'Cron Expression', cronExpr: 'Cron Expression',
cronExprDesc: 'Cron expression for workflow scheduling', cronExprDesc: 'Cron expression for workflow scheduling',
nameRequired: 'Please fill in all required information!', nameRequired: 'Please fill in complete information!',
addSuccess: 'Workflow created successfully. Please edit the process!', addSuccess: 'Task created successfully, please edit the workflow!',
addFailed: 'Failed to create workflow: {message}', addFailed: 'Failed to create task: {message}',
editSuccess: 'Workflow modified successfully!', editSuccess: 'Task modified successfully!',
editFailed: 'Failed to modify workflow: {message}', editFailed: 'Failed to modify task: {message}',
cancel: 'Cancel', cancel: 'Cancel',
confirm: 'Confirm', confirm: 'Confirm',
}, },
workflowActions: { workflowActions: {
title: 'Edit Workflow', title: 'Edit Workflow',
noActionsMessage: 'No actions in the workflow, please add actions', noActionsMessage: 'Workflow has no actions, please add actions',
addAction: 'Add Action', addAction: 'Add Action',
editAction: 'Edit Action', editAction: 'Edit Action',
deleteAction: 'Delete Action', deleteAction: 'Delete Action',
@@ -995,24 +1009,24 @@ export default {
confirmDeleteMessage: 'Are you sure you want to delete this action? This operation cannot be undone.', confirmDeleteMessage: 'Are you sure you want to delete this action? This operation cannot be undone.',
yesDelete: 'Yes, Delete', yesDelete: 'Yes, Delete',
noCancel: 'Cancel', noCancel: 'Cancel',
invalidConnection: 'Invalid connection: Cannot connect to self or same type port!', invalidConnection: 'Invalid connection: cannot connect to self or ports of the same type!',
componentNotFound: 'Component {component} not found', componentNotFound: 'Component {component} not found',
componentAdded: 'Component added to canvas', componentAdded: 'Component added to canvas',
saveSuccess: 'Workflow saved successfully!', saveSuccess: 'Task workflow saved successfully!',
saveFailed: 'Failed to save workflow: {message}', saveFailed: 'Failed to save task workflow: {message}',
importTitle: 'Import Workflow', importTitle: 'Import Task Workflow',
importSuccess: 'Import successful!', importSuccess: 'Import successful!',
importFailed: 'Import failed!', importFailed: 'Import failed!',
codeCopied: 'Workflow code copied to clipboard!', codeCopied: 'Task workflow code copied to clipboard!',
}, },
siteCookieUpdate: { siteCookieUpdate: {
title: 'Update Site Cookie', title: 'Update Site Cookie',
checkHint: 'Checking login status, please wait...', checkHint: 'Checking login status, please wait...',
confirmUpdateTitle: 'Confirm Update', confirmUpdateTitle: 'Confirm Update',
confirmUpdateMessage: "Do you want to update this site's cookie with the local cookie?", confirmUpdateMessage: 'Do you want to update this site\'s cookie with the local cookie?',
processing: 'Processing...', processing: 'Processing...',
success: 'Cookie updated successfully', success: 'Cookie updated successfully',
failed: 'Cookie update failed', failed: 'Failed to update cookie',
confirm: 'Confirm', confirm: 'Confirm',
cancel: 'Cancel', cancel: 'Cancel',
}, },
@@ -1024,7 +1038,7 @@ export default {
iconLabel: 'Site Icon', iconLabel: 'Site Icon',
uploadIcon: 'Upload Icon', uploadIcon: 'Upload Icon',
cookie: 'Cookie', cookie: 'Cookie',
rssUrl: 'RSS URL', rssUrl: 'RSS Link',
enableLabel: 'Enable', enableLabel: 'Enable',
pubEnableLabel: 'Public Resources', pubEnableLabel: 'Public Resources',
priorityLabel: 'Priority', priorityLabel: 'Priority',
@@ -1048,7 +1062,7 @@ export default {
title: 'Plugin Market Settings', title: 'Plugin Market Settings',
repoUrl: 'Plugin Repository URL', repoUrl: 'Plugin Repository URL',
repoPlaceholder: 'Format: https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/', repoPlaceholder: 'Format: https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
repoHint: 'Multiple addresses separated by commas, only supports Github repositories', repoHint: 'Multiple URLs separated by commas, only Github repositories are supported',
close: 'Close', close: 'Close',
save: 'Save', save: 'Save',
saveSuccess: 'Plugin repository saved successfully', saveSuccess: 'Plugin repository saved successfully',
@@ -1057,21 +1071,21 @@ export default {
userAuth: { userAuth: {
title: 'User Authentication', title: 'User Authentication',
codeLabel: 'Authentication Code', codeLabel: 'Authentication Code',
codePlaceholder: 'Please enter the authentication code', codePlaceholder: 'Please enter authentication code',
authBtn: 'Authenticate', authBtn: 'Start Authentication',
closeBtn: 'Close', closeBtn: 'Close',
selectSite: 'Select Authentication Site', selectSite: 'Select Authentication Site',
selectSiteRequired: 'Please select an authentication site!', selectSiteRequired: 'Please select authentication site!',
siteConfigNotExist: 'Site configuration does not exist!', siteConfigNotExist: 'Site configuration does not exist!',
fieldRequired: 'Please enter {name}!', fieldRequired: 'Please enter {name}!',
authSuccess: 'User authentication successful, please login again!', authSuccess: 'User authentication successful, please log in again!',
authFailed: 'Authentication failed: {message}', authFailed: 'Authentication failed: {message}',
}, },
transferQueue: { transferQueue: {
title: 'Transfer Queue', title: 'Organization Queue',
name: 'Name', name: 'Name',
type: 'Type', type: 'Type',
state: 'State', state: 'Status',
progress: 'Progress', progress: 'Progress',
startTime: 'Start Time', startTime: 'Start Time',
speedTitle: 'Speed', speedTitle: 'Speed',
@@ -1090,9 +1104,9 @@ export default {
close: 'Close', close: 'Close',
}, },
reorganize: { reorganize: {
title: 'Reorganize', title: 'Organize',
sourceTitle: 'Source Files', sourceTitle: 'Source File',
targetTitle: 'Target Files', targetTitle: 'Target File',
processingTitle: 'Processing', processingTitle: 'Processing',
confirmTitle: 'Confirm', confirmTitle: 'Confirm',
selectFile: 'Select File', selectFile: 'Select File',
@@ -1105,120 +1119,119 @@ export default {
selectTargetPath: 'Select Target Path', selectTargetPath: 'Select Target Path',
selectTargetDir: 'Select Target Directory', selectTargetDir: 'Select Target Directory',
selectFileName: 'Select Filename', selectFileName: 'Select Filename',
confirmMoving: 'Please confirm moving!', confirmMoving: 'Please confirm move!',
sourceLabel: 'Source:', sourceLabel: 'Source file:',
targetLabel: 'Target Directory:', targetLabel: 'Target directory:',
filenameLabel: 'Filename:', filenameLabel: 'Filename:',
close: 'Close', close: 'Close',
next: 'Next', next: 'Next',
previous: 'Previous', previous: 'Previous',
confirm: 'Confirm', confirm: 'Confirm',
manualTitle: 'Manual Reorganize', manualTitle: 'Manual Organization',
multipleItemsTitle: 'Reorganize - {count} Items', multipleItemsTitle: 'Organize - {count} Items',
singleItemTitle: 'Reorganize - {path}', singleItemTitle: 'Organize - {path}',
targetStorage: 'Target Storage', targetStorage: 'Target Storage',
targetStorageHint: 'Storage for reorganization', targetStorageHint: 'Organization target storage',
transferType: 'Transfer Method', transferType: 'Organization Method',
transferTypeHint: 'File operation method', transferTypeHint: 'File operation organization method',
targetPath: 'Target Path', targetPath: 'Target Path',
targetPathHint: 'Target path for reorganization, leave empty for automatic matching', targetPathHint: 'Organization target path, leave empty for auto-match',
targetPathPlaceholder: 'Leave empty for automatic', targetPathPlaceholder: 'Leave empty for auto',
mediaType: 'Type', mediaType: 'Type',
mediaTypeHint: 'Media type of the file', mediaTypeHint: 'File media type',
tmdbId: 'TheMovieDb ID', tmdbId: 'TheMovieDb ID',
doubanId: 'Douban ID', doubanId: 'Douban ID',
mediaIdHint: 'Search media ID by name, leave empty for automatic recognition', mediaIdHint: 'Query media ID by name, leave empty for auto recognition',
mediaIdPlaceholder: 'Leave empty for automatic recognition', mediaIdPlaceholder: 'Leave empty for auto recognition',
episodeGroup: 'Episode Group ID', episodeGroup: 'Episode Group ID',
episodeGroupHint: 'Specify episode group', episodeGroupHint: 'Specify episode group',
episodeGroupPlaceholder: 'Manually search episode group', episodeGroupPlaceholder: 'Manually query episode group',
season: 'Season', season: 'Season',
seasonHint: 'Which season', seasonHint: 'Which season',
episodeDetail: 'Episode', episodeDetail: 'Episode',
episodeDetailHint: 'Episode number or range, e.g., 1 or 1,2', episodeDetailHint: 'Episode number or range, e.g. 1 or 1,2',
episodeDetailPlaceholder: 'Start episode,end episode', episodeDetailPlaceholder: 'Start episode,End episode',
episodeFormat: 'Episode Format', episodeFormat: 'Episode Positioning',
episodeFormatHint: 'Use {ep} to locate the episode part in the filename for recognition', episodeFormatHint: 'Use {ep} to position episode number part in filename to assist recognition',
episodeFormatPlaceholder: 'Use {ep} to locate episode', episodeFormatPlaceholder: 'Use {ep} to position episode',
episodeOffset: 'Episode Offset', episodeOffset: 'Episode Offset',
episodeOffsetHint: 'Episode offset calculation, e.g., -10 or EP*2', episodeOffsetHint: 'Episode offset calculation, e.g. -10 or EP*2',
episodeOffsetPlaceholder: 'e.g., -10', episodeOffsetPlaceholder: 'e.g. -10',
episodePart: 'Specify Part', episodePart: 'Specify Part',
episodePartHint: 'Specify part, e.g., part1', episodePartHint: 'Specify part, e.g. part1',
episodePartPlaceholder: 'e.g., part1', episodePartPlaceholder: 'e.g. part1',
minFileSize: 'Minimum File Size (MB)', minFileSize: 'Min File Size (MB)',
minFileSizeHint: 'Only reorganize files larger than the minimum file size', minFileSizeHint: 'Only organize files larger than minimum file size',
typeFolderOption: 'Categorize by Type', typeFolderOption: 'Classify by Type',
typeFolderHint: 'Add subdirectories by media type in the target path during reorganization', typeFolderHint: 'Add subdirectory by media type in target path during organization',
categoryFolderOption: 'Categorize by Category', categoryFolderOption: 'Classify by Category',
categoryFolderHint: 'Add subdirectories by media category in the target path during reorganization', categoryFolderHint: 'Add subdirectory by media category in target path during organization',
scrapeOption: 'Scrape Metadata', scrapeOption: 'Scrape Metadata',
scrapeHint: 'Automatically scrape metadata after reorganization', scrapeHint: 'Automatically scrape metadata after organization',
fromHistoryOption: 'Reuse Historical Recognition Info', fromHistoryOption: 'Reuse Historical Recognition Info',
fromHistoryHint: 'Use media information recognized in historical reorganization records', fromHistoryHint: 'Use media info already recognized in historical organization records',
addToQueue: 'Add to Queue', addToQueue: 'Add to Organization Queue',
reorganizeNow: 'Reorganize Now', reorganizeNow: 'Organize Now',
auto: 'Auto', auto: 'Auto',
processing: 'Processing ...', processing: 'Processing ...',
successMessage: 'File {name} has been added to the reorganization queue!', successMessage: 'File {name} has been added to the organization queue!',
}, },
subscribeEdit: { subscribeEdit: {
titleDefault: 'Default Subscription Rule', titleDefault: 'Default Subscription Rules',
titleEditFormat: 'Edit Subscription - {name} {season}', titleEditFormat: 'Edit Subscription - {name} {season}',
seasonFormat: 'Season {number}', seasonFormat: 'Season {number}',
tabs: { tabs: {
basic: 'Basic', basic: 'Basic',
advance: 'Advanced', advance: 'Advanced',
}, },
searchKeyword: 'Search Keyword', searchKeyword: 'Search Keywords',
searchKeywordHint: 'Keyword used when searching sites', searchKeywordHint: 'Specify keywords used when searching sites',
totalEpisode: 'Total Episodes', totalEpisode: 'Total Episodes',
totalEpisodeHint: 'Total number of episodes', totalEpisodeHint: 'Total number of episodes',
startEpisode: 'Start Episode', startEpisode: 'Start Episode',
startEpisodeHint: 'Episode number to start subscription', startEpisodeHint: 'Starting episode number to subscribe',
quality: 'Quality', quality: 'Quality',
qualityHint: 'Resource quality for subscription', qualityHint: 'Subscription resource quality',
resolution: 'Resolution', resolution: 'Resolution',
resolutionHint: 'Resource resolution for subscription', resolutionHint: 'Subscription resource resolution',
effect: 'Effect', effect: 'Effects',
effectHint: 'Resource effect for subscription', effectHint: 'Subscription resource effects',
subscribeSites: 'Subscribe Sites', subscribeSites: 'Subscription Sites',
subscribeSitesHint: 'Sites to subscribe from, use system settings if none selected', subscribeSitesHint: 'Range of sites for subscription, use system settings if none selected',
downloader: 'Downloader', downloader: 'Downloader',
downloaderHint: 'Specify which downloader to use for this subscription', downloaderHint: 'Specify downloader for this subscription',
savePath: 'Save Path', savePath: 'Save Path',
savePathHint: 'Specify download path for this subscription, leave empty to use default directories', savePathHint: 'Specify download save path for this subscription, leave empty to use default download directory',
bestVersion: 'Version Upgrade', bestVersion: 'Version Upgrade',
bestVersionHint: 'Enable version upgrade based on priority', bestVersionHint: 'Perform version upgrade subscription based on upgrade priorities',
searchImdbid: 'Use ImdbID Search', searchImdbid: 'Search Using ImdbID',
searchImdbidHint: 'Use ImdbID for precise resource searching', searchImdbidHint: 'Use ImdbID for precise resource searching',
showEditDialog: 'Edit More Rules When Subscribing', showEditDialog: 'Edit More Rules When Subscribing',
showEditDialogHint: 'Show this edit dialog when adding subscription', showEditDialogHint: 'Show this edit subscription dialog when adding subscription',
include: 'Include (Keywords, Regex)', include: 'Include (Keywords, Regex)',
includeHint: 'Include rules, supports regex', includeHint: 'Include rules, supports regular expressions',
exclude: 'Exclude (Keywords, Regex)', exclude: 'Exclude (Keywords, Regex)',
excludeHint: 'Exclude rules, supports regex', excludeHint: 'Exclude rules, supports regular expressions',
filterGroups: 'Priority Rule Groups', filterGroups: 'Priority Rule Groups',
filterGroupsHint: 'Filter subscription based on selected rule groups', filterGroupsHint: 'Filter subscriptions by selected filter rule groups',
episodeGroup: 'Episode Group', episodeGroup: 'Specify Episode Group',
episodeGroupHint: 'Recognize and scrape based on specific episode group', episodeGroupHint: 'Recognize and scrape by specific episode group',
season: 'Season', season: 'Specify Season',
seasonHint: 'Specify season for subscription', seasonHint: 'Specify any season for subscription',
mediaCategory: 'Custom Category', mediaCategory: 'Custom Category',
mediaCategoryHint: 'Specify category name, auto-detect if empty', mediaCategoryHint: 'Specify category name, leave empty for auto-recognition',
customWords: 'Custom Recognition Words', customWords: 'Custom Recognition Words',
customWordsHint: 'Recognition words only for this subscription', customWordsHint: 'Recognition words only used for this subscription',
customWordsPlaceholder: customWordsPlaceholder: 'Block word\nReplaced word => Replacement word\nPrefix <> Suffix >> Episode offset (EP)\nReplaced word => Replacement word && Prefix <> Suffix >> Episode offset (EP)\nReplacement word supports format: &#123; tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx &#125; to directly specify TMDBID/Douban ID recognition, where s, e are season and episode numbers (optional)',
'Filter word\nReplaced word => Replacement\nFront position word <> Back position word >> Episode offset (EP)\nReplaced word => Replacement && Front position word <> Back position word >> Episode offset (EP)\nReplacement format: {[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]} to specify TMDBID/Douban ID, where s and e are season and episode (optional)',
cancelSubscribe: 'Cancel Subscription', cancelSubscribe: 'Cancel Subscription',
cancelSubscribeConfirm: 'Are you sure you want to cancel this subscription?',
save: 'Save', save: 'Save',
cancelSubscribeConfirm: 'Are you sure you want to cancel the subscription?',
}, },
subscribeFiles: { subscribeFiles: {
title: 'Downloaded Files', title: 'Downloaded Files',
noFilesMessage: 'No files', noFilesMessage: 'No files',
close: 'Close', close: 'Close',
downloadTab: 'Downloaded Files', downloadTab: 'Download Files',
libraryTab: 'Media Library Files', libraryTab: 'Media Library Files',
episodeColumn: 'Episode', episodeColumn: 'Episode',
torrentColumn: 'Torrent', torrentColumn: 'Torrent',
@@ -1262,28 +1275,136 @@ export default {
msgCount: 'Unread Messages', msgCount: 'Unread Messages',
inviteCount: 'Invites', inviteCount: 'Invites',
bonus: 'Bonus Points', bonus: 'Bonus Points',
ratio: 'Share Ratio', ratio: 'Ratio',
joinTime: 'Join Time', joinTime: 'Join Time',
trafficHistory: 'Traffic History', trafficHistory: 'Traffic History',
seedingDistribution: 'Seeding Distribution', seedingDistribution: 'Seeding Distribution',
volumeTitle: 'Volume', volumeTitle: 'Volume',
countTitle: 'Count: ', countTitle: 'Count:',
noData: 'None', noData: 'None',
refreshing: 'Refreshing site data...', refreshing: 'Refreshing site data...',
close: 'Close', close: 'Close',
}, },
siteResource: { siteResource: {
title: 'Site Resources', title: 'Site Resources',
searchHint: 'Search Resources', searchHint: 'Search resources',
close: 'Close', close: 'Close',
}, },
forkSubscribe: { forkSubscribe: {
title: 'Fork Subscription', title: 'Copy Subscription',
selectSubscriber: 'Select Target User', selectSubscriber: 'Select Copy Target',
overwriteExisting: 'Overwrite Existing', overwriteExisting: 'Overwrite Existing Subscription',
overwriteExistingHint: 'Whether to overwrite if the target user already has this subscription', overwriteExistingHint: 'Whether to overwrite when target user already has this subscription',
confirm: 'Confirm', confirm: 'Confirm',
cancel: 'Cancel', cancel: 'Cancel',
}, },
}, },
} file: {
newFolder: 'New Folder',
createFolder: 'Create Folder',
fileName: 'Filename',
fileSize: 'File Size',
fileType: 'File Type',
lastModified: 'Last Modified',
actions: 'Actions',
rename: 'Rename',
delete: 'Delete',
confirmDelete: 'Confirm Delete',
upload: 'Upload',
download: 'Download',
preview: 'Preview',
selectAll: 'Select All',
deselectAll: 'Deselect All',
moveUp: 'Go Up',
sortByName: 'Sort by Name',
sortByTime: 'Sort by Time',
currentName: 'Current Name',
newName: 'New Name',
includeSubfolders: 'Auto rename all media files in directory',
emptyFolder: 'Empty Folder',
noFilesInFolder: 'No files in this folder',
autoRecognize: 'Auto Recognize Name',
directoryTree: 'Directory Tree',
rootDirectory: 'Root Directory',
noDirectories: 'No directories available',
},
person: {
alias: 'Also Known As:',
credits: 'Credits',
biography: 'Biography',
birthday: 'Birthday',
placeOfBirth: 'Place of Birth',
},
error: {
title: 'Error!',
networkError: 'Unable to get media information, please check your network connection.',
serverError: 'Server error, please try again later.',
notFound: 'Requested resource not found.',
},
plugin: {
sort: {
popular: 'Popular',
name: 'Plugin Name',
author: 'Author',
repository: 'Plugin Repository',
latest: 'Latest Release',
},
installingPlugin: 'Installing plugin...',
installing: 'Installing {name} v{version} ...',
installSuccess: 'Plugin {name} installed successfully!',
installFailed: 'Plugin {name} installation failed: {message}',
uninstalling: 'Uninstalling {name} ...',
uninstallSuccess: 'Plugin {name} uninstalled successfully!',
uninstallFailed: 'Plugin {name} uninstallation failed: {message}',
updating: 'Updating {name} to v{version} ...',
updateSuccess: 'Plugin {name} updated successfully!',
updateFailed: 'Plugin {name} update failed: {message}',
noPlugins: 'No plugins installed',
installed: 'Installed',
notInstalled: 'Not Installed',
hasUpdate: 'Update Available',
configuring: 'Configuring',
enable: 'Enable',
disable: 'Disable',
settings: 'Settings',
},
profile: {
personalInfo: 'Personal Information',
uploadNewAvatar: 'Upload New Avatar',
avatarFormatError: 'The uploaded file does not meet requirements, please select a new avatar',
avatarSizeError: 'File size must not exceed 800KB',
avatarUploadSuccess: 'New avatar uploaded successfully, will take effect after saving!',
resetAvatarSuccess: 'Reset to default avatar, will take effect after saving!',
restoreAvatarSuccess: 'Restored current avatar!',
savingInProgress: 'Saving in progress, please wait...',
usernameRequired: 'Username cannot be empty',
passwordMismatch: 'The two passwords do not match',
usernameChangeSuccess: '[{oldName}] renamed to [{newName}], user information saved successfully!',
saveSuccess: 'User information saved successfully!',
saveFailedWithNameChange: '[{oldName}] renamed to [{newName}], information save failed: {message}!',
saveFailed: 'User information save failed: {message}!',
nickname: 'Nickname',
nicknamePlaceholder: 'Display nickname, takes precedence over username',
accountBinding: 'Account Binding',
wechatUser: 'WeChat User',
telegramUser: 'Telegram User',
slackUser: 'Slack User',
vocechatUser: 'VoceChat User',
synologychatUser: 'SynologyChat User',
doubanUser: 'Douban User',
twoFactorAuthentication: 'Two-Factor Authentication',
enableTwoFactor: 'Enable Two-Factor Authentication',
disableTwoFactor: 'Disable Two-Factor Authentication',
otpGenerateFailed: 'Failed to get OTP URI: {message}!',
otpDisableSuccess: 'Two-factor authentication disabled successfully!',
otpDisableFailed: 'Failed to disable OTP: {message}!',
otpCodeRequired: 'Please enter the 6-digit verification code',
otpEnableSuccess: 'Two-factor authentication enabled successfully!',
otpEnableFailed: 'Failed to enable OTP: {message}!',
authenticatorApp: 'Authenticator App',
authenticatorAppDescription: 'Use an authenticator app like Google Authenticator, Microsoft Authenticator, Authy, or 1Password to scan the QR code. It will generate a 6-digit code for you to enter below.',
secretKeyTip: 'If you\'re having trouble with the QR code, select manual entry in your app and enter the code above.',
enterVerificationCode: 'Enter verification code to confirm enabling two-factor authentication',
avatarFormatTip: 'JPG, PNG, GIF, WEBP formats allowed, maximum size 800KB.',
},
}
+128 -1
View File
@@ -17,6 +17,10 @@ export default {
noContent: '没有找到相关内容', noContent: '没有找到相关内容',
all: '全部', all: '全部',
default: '默认', default: '默认',
name: '名称',
create: '新建',
saving: '保存中',
reset: '重置',
}, },
theme: { theme: {
light: '浅色', light: '浅色',
@@ -161,6 +165,20 @@ export default {
siteAuth: '用户认证', siteAuth: '用户认证',
helpDocs: '帮助文档', helpDocs: '帮助文档',
restart: '重启', restart: '重启',
management: '用户管理',
noUsers: '没有用户',
clickToAddUser: '点击添加用户卡片添加用户',
addUser: '添加用户',
editUser: '编辑用户',
username: '用户名',
password: '密码',
confirmPassword: '确认密码',
role: '角色',
email: '邮箱',
enabled: '启用',
disabled: '禁用',
status: '状态',
operations: '操作',
}, },
nav: { nav: {
more: '更多', more: '更多',
@@ -1187,7 +1205,7 @@ export default {
customWords: '自定义识别词', customWords: '自定义识别词',
customWordsHint: '只对该订阅使用的识别词', customWordsHint: '只对该订阅使用的识别词',
customWordsPlaceholder: customWordsPlaceholder:
'屏蔽词\n被替换词 => 替换词\n前定位词 <> 后定位词 >> 集偏移量(EP\n被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量(EP)\n其中替换词支持格式:{[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]} 直接指定TMDBID/豆瓣ID识别,其中s、e为季数和集数(可选)', '屏蔽词\n被替换词 => 替换词\n前定位词 <> 后定位词 >> 集偏移量(EP\n被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量(EP)\n其中替换词支持格式:&#123; tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx &#125; 直接指定TMDBID/豆瓣ID识别,其中s、e为季数和集数(可选)',
cancelSubscribe: '取消订阅', cancelSubscribe: '取消订阅',
save: '保存', save: '保存',
cancelSubscribeConfirm: '是否确认取消订阅?', cancelSubscribeConfirm: '是否确认取消订阅?',
@@ -1264,4 +1282,113 @@ export default {
cancel: '取消', cancel: '取消',
}, },
}, },
file: {
newFolder: '新建文件夹',
createFolder: '创建文件夹',
fileName: '文件名',
fileSize: '文件大小',
fileType: '文件类型',
lastModified: '修改时间',
actions: '操作',
rename: '重命名',
delete: '删除',
confirmDelete: '确认删除',
upload: '上传',
download: '下载',
preview: '预览',
selectAll: '全选',
deselectAll: '取消全选',
moveUp: '返回上一级',
sortByName: '按名称排序',
sortByTime: '按时间排序',
currentName: '当前名称',
newName: '新名称',
includeSubfolders: '自动重命名目录内所有媒体文件',
emptyFolder: '空文件夹',
noFilesInFolder: '该文件夹内没有文件',
autoRecognize: '自动识别名称',
directoryTree: '目录树',
rootDirectory: '根目录',
noDirectories: '没有可用的目录',
},
person: {
alias: '别名:',
credits: '参演作品',
biography: '个人简介',
birthday: '出生日期',
placeOfBirth: '出生地',
},
error: {
title: '出错啦!',
networkError: '无法获取到媒体信息,请检查网络连接。',
serverError: '服务器错误,请稍后重试。',
notFound: '找不到请求的资源。',
},
plugin: {
sort: {
popular: '热门',
name: '插件名称',
author: '作者',
repository: '插件仓库',
latest: '最新发布',
},
installingPlugin: '正在安装插件...',
installing: '正在安装 {name} v{version} ...',
installSuccess: '插件 {name} 安装成功!',
installFailed: '插件 {name} 安装失败:{message}',
uninstalling: '正在卸载 {name} ...',
uninstallSuccess: '插件 {name} 卸载成功!',
uninstallFailed: '插件 {name} 卸载失败:{message}',
updating: '正在更新 {name} 至 v{version} ...',
updateSuccess: '插件 {name} 更新成功!',
updateFailed: '插件 {name} 更新失败:{message}',
noPlugins: '没有安装插件',
installed: '已安装',
notInstalled: '未安装',
hasUpdate: '有更新',
configuring: '配置',
enable: '启用',
disable: '禁用',
settings: '设置',
},
profile: {
personalInfo: '个人信息',
uploadNewAvatar: '上传新头像',
avatarFormatError: '上传的文件不符合要求,请重新选择头像',
avatarSizeError: '文件大小不得大于800KB',
avatarUploadSuccess: '新头像上传成功,待保存后生效!',
resetAvatarSuccess: '已重置为默认头像,待保存后生效!',
restoreAvatarSuccess: '已还原当前使用头像!',
savingInProgress: '正在保存中,请稍后...',
usernameRequired: '用户名不能为空',
passwordMismatch: '两次输入的密码不一致',
usernameChangeSuccess: '【{oldName}】更名【{newName}】,用户信息保存成功!',
saveSuccess: '用户信息保存成功!',
saveFailedWithNameChange: '【{oldName}】更名【{newName}】,信息保存失败:{message}',
saveFailed: '用户信息保存失败:{message}',
nickname: '昵称',
nicknamePlaceholder: '显示昵称,优先于用户名显示',
accountBinding: '账号绑定',
wechatUser: '微信用户',
telegramUser: 'Telegram用户',
slackUser: 'Slack用户',
vocechatUser: 'VoceChat用户',
synologychatUser: 'SynologyChat用户',
doubanUser: '豆瓣用户',
twoFactorAuthentication: '登录双重验证',
enableTwoFactor: '开启双重验证',
disableTwoFactor: '关闭双重验证',
otpGenerateFailed: '获取otp uri失败:{message}',
otpDisableSuccess: '关闭登录双重验证成功!',
otpDisableFailed: '关闭otp失败:{message}',
otpCodeRequired: '请填写6位验证码',
otpEnableSuccess: '开启登录双重验证成功!',
otpEnableFailed: '开启otp失败:{message}',
authenticatorApp: '身份验证器',
authenticatorAppDescription:
'使用像Google Authenticator、Microsoft Authenticator、Authy或1Password这样的身份验证器应用程序,扫描二维码。它将为您生成一个6位数的代码,供您在下方输入。',
secretKeyTip: '如果您在使用二维码时遇到困难,请在您的应用程序中选择手动输入以上代码。',
enterVerificationCode: '输入验证码以确认开启双重验证',
avatarFormatTip: '允许 JPG、PNG、GIF、WEBP 格式, 最大尺寸 800KB。',
},
} }
+10 -17
View File
@@ -1,31 +1,24 @@
<script setup lang="ts"> <script setup lang="ts">
import PersonCardListView from '@/views/discover/PersonCardListView.vue' import PersonCardListView from '@/views/discover/PersonCardListView.vue'
// 输入参数
const props = defineProps({
// API路径
paths: Array as PropType<string[]> | PropType<string>,
})
// 路由参数 // 路由参数
const route = useRoute() const route = useRoute()
const id = route.query?.id?.toString()
// 标题
const title = route.query?.title?.toString() const title = route.query?.title?.toString()
const source = route.query?.source?.toString()
// 类型
const type = route.query?.type?.toString() const type = route.query?.type?.toString()
const apipath = route.query?.apipath?.toString()
// 计算API路径
function getApiPath(paths: string[] | string) {
if (Array.isArray(paths)) return paths.join('/')
else return paths
}
</script> </script>
<template> <template>
<div> <div>
<VPageContentTitle :title="title" /> <VPageContentTitle :title="title" />
<PersonCardListView :apipath="getApiPath(props.paths || '')" :params="route.query" :type="type" /> <PersonCardListView
:credits-id="id"
:credits-name="title"
:credits-source="source"
:credits-type="type"
:credits-apipath="apipath"
/>
</div> </div>
</template> </template>
+3 -1
View File
@@ -3,5 +3,7 @@ import TransferHistoryView from '@/views/reorganize/TransferHistoryView.vue'
</script> </script>
<template> <template>
<TransferHistoryView /> <div>
<TransferHistoryView />
</div>
</template> </template>
+13 -3
View File
@@ -1,15 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import MediaDetailView from '@/views/discover/MediaDetailView.vue' import MediaDetailView from '@/views/discover/MediaDetailView.vue'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 路由参数 // 路由参数
const route = useRoute() const route = useRoute()
// TMDBID // TMDB ID
const mediaid = route.query?.mediaid?.toString() const mediaid = route.query?.mediaid?.toString()
// 类型 // 类型:电影、电视剧
const type = route.query?.type?.toString() const type = route.query?.type?.toString()
// 媒体信息来源:TMDB、豆瓣
const source = route.query?.source?.toString() || 'themoviedb'
// TMDB ID
const page = route.query?.page?.toString() || '1'
// 标题 // 标题
const title = route.query?.title?.toString() const title = route.query?.title?.toString()
@@ -19,6 +29,6 @@ const year = route.query?.year?.toString()
<template> <template>
<div> <div>
<MediaDetailView :mediaid="mediaid" :type="type" :title="title" :year="year" /> <MediaDetailView :mediaid="mediaid" :type="type" :source="source" :page="page" :title="title" :year="year" />
</div> </div>
</template> </template>
+2 -2
View File
@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import SiteListView from '@/views/site/SiteCardListView.vue' import SiteCardListView from '@/views/site/SiteCardListView.vue'
</script> </script>
<template> <template>
<div> <div>
<SiteListView /> <SiteCardListView />
</div> </div>
</template> </template>
+3 -1
View File
@@ -3,5 +3,7 @@ import WorkflowListView from '@/views/workflow/WorkflowListView.vue'
</script> </script>
<template> <template>
<WorkflowListView /> <div>
<WorkflowListView />
</div>
</template> </template>
+9 -5
View File
@@ -4,6 +4,10 @@ import api from '@/api'
import personIcon from '@images/misc/person.png' import personIcon from '@images/misc/person.png'
import type { Person } from '@/api/types' import type { Person } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue' import NoDataFound from '@/components/NoDataFound.vue'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 输入参数 // 输入参数
const personProps = defineProps({ const personProps = defineProps({
@@ -67,7 +71,7 @@ function getPersonImage() {
function getAlsoKnownAs() { function getAlsoKnownAs() {
if (!personDetail.value?.also_known_as) return '' if (!personDetail.value?.also_known_as) return ''
if (personProps.source === 'themoviedb') { if (personProps.source === 'themoviedb') {
return '别名:' + personDetail.value.also_known_as.join('、') return t('person.alias') + personDetail.value.also_known_as.join('、')
} else { } else {
return personDetail.value.also_known_as.join('') return personDetail.value.also_known_as.join('')
} }
@@ -81,7 +85,7 @@ function getPersonCreditsPath() {
} else if (personProps.source === 'bangumi') { } else if (personProps.source === 'bangumi') {
apipath = 'bangumi' apipath = 'bangumi'
} }
return `/browse/${apipath}/person/credits/${personDetail.value.id}?title=参演作品` return `/browse/${apipath}/person/credits/${personDetail.value.id}?title=${t('person.credits')}`
} }
// 参演作品API路径 // 参演作品API路径
@@ -136,7 +140,7 @@ onBeforeMount(() => {
<div> <div>
<div class="slider-header"> <div class="slider-header">
<RouterLink :to="getPersonCreditsPath()" class="slider-title"> <RouterLink :to="getPersonCreditsPath()" class="slider-title">
<span>参演作品</span> <span>{{ t('person.credits') }}</span>
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" /> <VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
</RouterLink> </RouterLink>
</div> </div>
@@ -146,7 +150,7 @@ onBeforeMount(() => {
<NoDataFound <NoDataFound
v-if="!personDetail.id && isRefreshed" v-if="!personDetail.id && isRefreshed"
error-code="500" error-code="500"
error-title="出错啦" :error-title="t('error.title')"
error-description="无法获取到媒体信息请检查网络连接" :error-description="t('error.networkError')"
/> />
</template> </template>
+15 -11
View File
@@ -12,6 +12,10 @@ import { isNullOrEmptyObject } from '@/@core/utils'
import { getPluginTabs } from '@/router/i18n-menu' import { getPluginTabs } from '@/router/i18n-menu'
import PluginMarketSettingDialog from '@/components/dialog/PluginMarketSettingDialog.vue' import PluginMarketSettingDialog from '@/components/dialog/PluginMarketSettingDialog.vue'
import { useDynamicButton } from '@/composables/useDynamicButton' import { useDynamicButton } from '@/composables/useDynamicButton'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
const route = useRoute() const route = useRoute()
@@ -37,13 +41,13 @@ const activeSort = ref(null)
const orderConfig = ref<{ id: string }[]>([]) const orderConfig = ref<{ id: string }[]>([])
// 排序选项 // 排序选项
const sortOptions = [ const sortOptions = computed(() => [
{ title: '热门', value: 'count' }, { title: t('plugin.sort.popular'), value: 'count' },
{ title: '插件名称', value: 'plugin_name' }, { title: t('plugin.sort.name'), value: 'plugin_name' },
{ title: '作者', value: 'plugin_author' }, { title: t('plugin.sort.author'), value: 'plugin_author' },
{ title: '插件仓库', value: 'repo_url' }, { title: t('plugin.sort.repository'), value: 'repo_url' },
{ title: '最新发布', value: 'add_time' }, { title: t('plugin.sort.latest'), value: 'add_time' },
] ])
// 加载中 // 加载中
const loading = ref(false) const loading = ref(false)
@@ -105,7 +109,7 @@ const $toast = useToast()
const progressDialog = ref(false) const progressDialog = ref(false)
// 进度框文本 // 进度框文本
const progressText = ref('正在安装插件...') const progressText = ref(t('plugin.installingPlugin'))
// 过滤表单 // 过滤表单
const filterForm = reactive({ const filterForm = reactive({
@@ -217,7 +221,7 @@ async function installPlugin(item: Plugin) {
try { try {
// 显示等待提示框 // 显示等待提示框
progressDialog.value = true progressDialog.value = true
progressText.value = `正在安装 ${item?.plugin_name} v${item?.plugin_version} ...` progressText.value = t('plugin.installing', { name: item?.plugin_name, version: item?.plugin_version })
const result: { [key: string]: any } = await api.get(`plugin/install/${item?.id}`, { const result: { [key: string]: any } = await api.get(`plugin/install/${item?.id}`, {
params: { params: {
@@ -230,12 +234,12 @@ async function installPlugin(item: Plugin) {
progressDialog.value = false progressDialog.value = false
if (result.success) { if (result.success) {
$toast.success(`插件 ${item?.plugin_name} 安装成功!`) $toast.success(t('plugin.installSuccess', { name: item?.plugin_name }))
// 刷新 // 刷新
refreshData() refreshData()
} else { } else {
$toast.error(`插件 ${item?.plugin_name} 安装失败:${result.message}`) $toast.error(t('plugin.installFailed', { name: item?.plugin_name, message: result.message }))
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
+5 -1
View File
@@ -2,6 +2,10 @@
import api from '@/api' import api from '@/api'
import { FileItem, StorageConf, TransferDirectoryConf } from '@/api/types' import { FileItem, StorageConf, TransferDirectoryConf } from '@/api/types'
import FileBrowser from '@/components/FileBrowser.vue' import FileBrowser from '@/components/FileBrowser.vue'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
const endpoints = { const endpoints = {
list: { list: {
@@ -179,7 +183,7 @@ onMounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.file-browser-view { .file-browser-view {
height: 100%;
position: relative; position: relative;
block-size: 100%;
} }
</style> </style>
+7 -2
View File
@@ -6,6 +6,11 @@ import UserCard from '@/components/cards/UserCard.vue'
import UserAddEditDialog from '@/components/dialog/UserAddEditDialog.vue' import UserAddEditDialog from '@/components/dialog/UserAddEditDialog.vue'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import { useDynamicButton } from '@/composables/useDynamicButton' import { useDynamicButton } from '@/composables/useDynamicButton'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// APP // APP
const display = useDisplay() const display = useDisplay()
const appMode = inject('pwaMode') && display.mdAndDown.value const appMode = inject('pwaMode') && display.mdAndDown.value
@@ -68,7 +73,7 @@ useDynamicButton({
<template> <template>
<!-- 页面标题 --> <!-- 页面标题 -->
<VPageContentTitle title="用户管理" /> <VPageContentTitle :title="t('user.management')" />
<div class="card-list-container"> <div class="card-list-container">
<!-- 加载中提示 --> <!-- 加载中提示 -->
<LoadingBanner v-if="!isRefreshed" class="mt-12" /> <LoadingBanner v-if="!isRefreshed" class="mt-12" />
@@ -87,7 +92,7 @@ useDynamicButton({
<!-- 无数据提示 --> <!-- 无数据提示 -->
<div v-if="allUsers.length === 0 && isRefreshed"> <div v-if="allUsers.length === 0 && isRefreshed">
<NoDataFound error-code="404" error-title="没有用户" error-description="点击添加用户卡片添加用户" /> <NoDataFound error-code="404" :error-title="t('user.noUsers')" :error-description="t('user.clickToAddUser')" />
</div> </div>
<!-- 新增用户按钮 --> <!-- 新增用户按钮 -->
+62 -53
View File
@@ -7,6 +7,10 @@ import type { User } from '@/api/types'
import avatar1 from '@images/avatars/avatar-1.png' import avatar1 from '@images/avatars/avatar-1.png'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import { useUserStore } from '@/stores' import { useUserStore } from '@/stores'
import { useI18n } from 'vue-i18n'
//
const { t } = useI18n()
// //
const display = useDisplay() const display = useDisplay()
@@ -73,19 +77,19 @@ function changeAvatar(file: Event) {
const maxSize = 800 * 1024 const maxSize = 800 * 1024
// //
if (!allowedTypes.includes(selectedFile.type)) { if (!allowedTypes.includes(selectedFile.type)) {
$toast.error('上传的文件不符合要求,请重新选择头像') $toast.error(t('profile.avatarFormatError'))
return return
} }
// //
if (selectedFile.size > maxSize) { if (selectedFile.size > maxSize) {
$toast.error('文件大小不得大于800KB') $toast.error(t('profile.avatarSizeError'))
return return
} }
fileReader.readAsDataURL(selectedFile) fileReader.readAsDataURL(selectedFile)
fileReader.onload = () => { fileReader.onload = () => {
if (typeof fileReader.result === 'string') { if (typeof fileReader.result === 'string') {
currentAvatar.value = fileReader.result currentAvatar.value = fileReader.result
$toast.success('新头像上传成功,待保存后生效!') $toast.success(t('profile.avatarUploadSuccess'))
} }
} }
} }
@@ -94,13 +98,13 @@ function changeAvatar(file: Event) {
// //
function resetDefaultAvatar() { function resetDefaultAvatar() {
currentAvatar.value = avatar1 currentAvatar.value = avatar1
$toast.success('已重置为默认头像,待保存后生效!') $toast.success(t('profile.resetAvatarSuccess'))
} }
// //
function restoreCurrentAvatar() { function restoreCurrentAvatar() {
currentAvatar.value = accountInfo.value.avatar currentAvatar.value = accountInfo.value.avatar
$toast.success('已还原当前使用头像!') $toast.success(t('profile.restoreAvatarSuccess'))
} }
// //
@@ -121,16 +125,16 @@ async function fetchUserInfo() {
// //
async function saveAccountInfo() { async function saveAccountInfo() {
if (isSaving.value) { if (isSaving.value) {
$toast.error('正在保存中,请稍后...') $toast.error(t('profile.savingInProgress'))
return return
} }
if (!currentUserName.value) { if (!currentUserName.value) {
$toast.error('用户名不能为空') $toast.error(t('profile.usernameRequired'))
return return
} }
if (newPassword.value || confirmPassword.value) { if (newPassword.value || confirmPassword.value) {
if (newPassword.value !== confirmPassword.value) { if (newPassword.value !== confirmPassword.value) {
$toast.error('两次输入的密码不一致') $toast.error(t('profile.passwordMismatch'))
return return
} }
accountInfo.value.password = newPassword.value accountInfo.value.password = newPassword.value
@@ -157,11 +161,11 @@ async function saveAccountInfo() {
if (result.success) { if (result.success) {
if (oldUserName !== currentUserName.value) { if (oldUserName !== currentUserName.value) {
$toast.success(`${oldUserName}】更名【${currentUserName.value}】,用户信息保存成功!`) $toast.success(t('profile.usernameChangeSuccess', { oldName: oldUserName, newName: currentUserName.value }))
// //
userStore.setUserName(currentUserName.value) userStore.setUserName(currentUserName.value)
} else { } else {
$toast.success('用户信息保存成功!') $toast.success(t('profile.saveSuccess'))
} }
// //
if (oldAvatar !== currentAvatar.value) { if (oldAvatar !== currentAvatar.value) {
@@ -169,9 +173,15 @@ async function saveAccountInfo() {
} }
} else { } else {
if (oldAvatar !== currentAvatar.value) { if (oldAvatar !== currentAvatar.value) {
$toast.error(`${oldUserName}】更名【${currentUserName.value}】,信息保存失败:${result.message}`) $toast.error(
t('profile.saveFailedWithNameChange', {
oldName: oldUserName,
newName: currentUserName.value,
message: result.message,
}),
)
} else { } else {
$toast.error(`用户信息保存失败:${result.message}`) $toast.error(t('profile.saveFailed', { message: result.message }))
} }
// //
currentUserName.value = accountInfo.value.name currentUserName.value = accountInfo.value.name
@@ -195,7 +205,7 @@ async function getOtpUri() {
qrCode.value = result.data.uri qrCode.value = result.data.uri
otpDialog.value = true otpDialog.value = true
} else { } else {
$toast.error(`获取otp uri失败:${result.message}`) $toast.error(t('profile.otpGenerateFailed', { message: result.message }))
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error)
@@ -208,9 +218,9 @@ async function disableOtp() {
const result: { [key: string]: any } = await api.post('user/otp/disable') const result: { [key: string]: any } = await api.post('user/otp/disable')
if (result.success) { if (result.success) {
accountInfo.value.is_otp = false accountInfo.value.is_otp = false
$toast.success('关闭登录双重验证成功!') $toast.success(t('profile.otpDisableSuccess'))
} else { } else {
$toast.error(`关闭otp失败:${result.message}`) $toast.error(t('profile.otpDisableFailed', { message: result.message }))
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error)
@@ -220,7 +230,7 @@ async function disableOtp() {
// Otp // Otp
async function judgeOtpPassword() { async function judgeOtpPassword() {
if (!otpPassword.value) { if (!otpPassword.value) {
$toast.error('请填写6位验证码') $toast.error(t('profile.otpCodeRequired'))
return return
} }
try { try {
@@ -230,11 +240,11 @@ async function judgeOtpPassword() {
}) })
if (result.success) { if (result.success) {
$toast.success('开启登录双重验证成功!') $toast.success(t('profile.otpEnableSuccess'))
otpDialog.value = false otpDialog.value = false
accountInfo.value.is_otp = true accountInfo.value.is_otp = true
} else { } else {
$toast.error(`开启otp失败:${result.message}`) $toast.error(t('profile.otpEnableFailed', { message: result.message }))
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error)
@@ -259,7 +269,7 @@ watch(
<div> <div>
<VRow> <VRow>
<VCol cols="12"> <VCol cols="12">
<VCard title="个人信息"> <VCard :title="t('profile.personalInfo')">
<VCardText class="flex"> <VCardText class="flex">
<!-- 👉 Avatar --> <!-- 👉 Avatar -->
<VAvatar rounded="lg" size="100" class="me-6" :image="currentAvatar" /> <VAvatar rounded="lg" size="100" class="me-6" :image="currentAvatar" />
@@ -269,7 +279,7 @@ watch(
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<VBtn color="primary" @click="refInputEl?.click()"> <VBtn color="primary" @click="refInputEl?.click()">
<VIcon icon="mdi-cloud-upload-outline" /> <VIcon icon="mdi-cloud-upload-outline" />
<span v-if="display.mdAndUp.value" class="ms-2">上传新头像</span> <span v-if="display.mdAndUp.value" class="ms-2">{{ t('profile.uploadNewAvatar') }}</span>
</VBtn> </VBtn>
<input <input
@@ -283,12 +293,12 @@ watch(
<VBtn type="reset" color="info" variant="tonal" @click="restoreCurrentAvatar"> <VBtn type="reset" color="info" variant="tonal" @click="restoreCurrentAvatar">
<VIcon icon="mdi-refresh" /> <VIcon icon="mdi-refresh" />
<span v-if="display.mdAndUp.value" class="ms-2">重置</span> <span v-if="display.mdAndUp.value" class="ms-2">{{ t('common.reset') }}</span>
</VBtn> </VBtn>
<VBtn type="reset" color="error" variant="tonal" @click="resetDefaultAvatar"> <VBtn type="reset" color="error" variant="tonal" @click="resetDefaultAvatar">
<VIcon icon="mdi-image-sync-outline" /> <VIcon icon="mdi-image-sync-outline" />
<span v-if="display.mdAndUp.value" class="ms-2">默认</span> <span v-if="display.mdAndUp.value" class="ms-2">{{ t('common.default') }}</span>
</VBtn> </VBtn>
<VBtn <VBtn
@@ -298,12 +308,12 @@ watch(
> >
<VIcon icon="mdi-account-key" /> <VIcon icon="mdi-account-key" />
<span v-if="display.mdAndUp.value" class="ms-2">{{ <span v-if="display.mdAndUp.value" class="ms-2">{{
accountInfo.is_otp ? '关闭双重验证' : '开启双重验证' accountInfo.is_otp ? t('profile.disableTwoFactor') : t('profile.enableTwoFactor')
}}</span> }}</span>
</VBtn> </VBtn>
</div> </div>
<p class="text-body-1 mb-0">允许 JPGPNGGIFWEBP 格式 最大尺寸 800KB</p> <p class="text-body-1 mb-0">{{ t('profile.avatarFormatTip') }}</p>
</form> </form>
</VCardText> </VCardText>
@@ -312,10 +322,16 @@ watch(
<VForm class="mt-6"> <VForm class="mt-6">
<VRow> <VRow>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField v-model="currentUserName" density="comfortable" readonly label="用户名" /> <VTextField v-model="currentUserName" density="comfortable" readonly :label="t('user.username')" />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField v-model="accountInfo.email" density="comfortable" clearable label="邮箱" type="email" /> <VTextField
v-model="accountInfo.email"
density="comfortable"
clearable
:label="t('user.email')"
type="email"
/>
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
@@ -324,7 +340,7 @@ watch(
:type="isNewPasswordVisible ? 'text' : 'password'" :type="isNewPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isNewPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'" :append-inner-icon="isNewPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
clearable clearable
label="密码" :label="t('user.password')"
autocomplete="" autocomplete=""
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible" @click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
/> />
@@ -337,7 +353,7 @@ watch(
:type="isConfirmPasswordVisible ? 'text' : 'password'" :type="isConfirmPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'" :append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
clearable clearable
label="确认密码" :label="t('user.confirmPassword')"
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible" @click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
/> />
</VCol> </VCol>
@@ -346,14 +362,14 @@ watch(
v-model="accountInfo.nickname" v-model="accountInfo.nickname"
density="comfortable" density="comfortable"
clearable clearable
label="昵称" :label="t('profile.nickname')"
placeholder="显示昵称,优先于用户名显示" :placeholder="t('profile.nicknamePlaceholder')"
/> />
</VCol> </VCol>
</VRow> </VRow>
<VDivider class="my-10"> <VDivider class="my-10">
<span>账号绑定</span> <span>{{ t('profile.accountBinding') }}</span>
</VDivider> </VDivider>
<VRow> <VRow>
@@ -362,7 +378,7 @@ watch(
v-model="accountInfo.settings.wechat_userid" v-model="accountInfo.settings.wechat_userid"
density="comfortable" density="comfortable"
clearable clearable
label="微信用户" :label="t('profile.wechatUser')"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -370,7 +386,7 @@ watch(
v-model="accountInfo.settings.telegram_userid" v-model="accountInfo.settings.telegram_userid"
density="comfortable" density="comfortable"
clearable clearable
label="Telegram用户" :label="t('profile.telegramUser')"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -378,7 +394,7 @@ watch(
v-model="accountInfo.settings.slack_userid" v-model="accountInfo.settings.slack_userid"
density="comfortable" density="comfortable"
clearable clearable
label="Slack用户" :label="t('profile.slackUser')"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -386,7 +402,7 @@ watch(
v-model="accountInfo.settings.vocechat_userid" v-model="accountInfo.settings.vocechat_userid"
density="comfortable" density="comfortable"
clearable clearable
label="VoceChat用户" :label="t('profile.vocechatUser')"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -394,7 +410,7 @@ watch(
v-model="accountInfo.settings.synologychat_userid" v-model="accountInfo.settings.synologychat_userid"
density="comfortable" density="comfortable"
clearable clearable
label="SynologyChat用户" :label="t('profile.synologychatUser')"
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
@@ -402,7 +418,7 @@ watch(
v-model="accountInfo.settings.douban_userid" v-model="accountInfo.settings.douban_userid"
density="comfortable" density="comfortable"
clearable clearable
label="豆瓣用户" :label="t('profile.doubanUser')"
/> />
</VCol> </VCol>
</VRow> </VRow>
@@ -410,8 +426,8 @@ watch(
<!-- 👉 Form Actions --> <!-- 👉 Form Actions -->
<VCol cols="12" class="d-flex flex-wrap gap-4"> <VCol cols="12" class="d-flex flex-wrap gap-4">
<VBtn @click="saveAccountInfo" :disabled="isSaving"> <VBtn @click="saveAccountInfo" :disabled="isSaving">
<span v-if="isSaving">保存中...</span> <span v-if="isSaving">{{ t('common.saving') }}...</span>
<span v-else>保存</span> <span v-else>{{ t('common.save') }}</span>
</VBtn> </VBtn>
</VCol> </VCol>
</VRow> </VRow>
@@ -427,40 +443,33 @@ watch(
<VCard> <VCard>
<VDialogCloseBtn @click="otpDialog = false" /> <VDialogCloseBtn @click="otpDialog = false" />
<VCardText> <VCardText>
<h4 class="text-h4 text-center mb-6 mt-5">登录双重验证</h4> <h4 class="text-h4 text-center mb-6 mt-5">{{ t('profile.twoFactorAuthentication') }}</h4>
<h5 class="text-h5 font-weight-medium mb-2">身份验证器</h5> <h5 class="text-h5 font-weight-medium mb-2">{{ t('profile.authenticatorApp') }}</h5>
<p class="mb-6"> <p class="mb-6">
使用像Google AuthenticatorMicrosoft {{ t('profile.authenticatorAppDescription') }}
AuthenticatorAuthy或1Password这样的身份验证器应用程序扫描二维码它将为您生成一个6位数的代码供您在下方输入
</p> </p>
<div class="my-6"> <div class="my-6">
<QrcodeVue class="mx-auto" :value="qrCode" :size="200" max-width="25rem" /> <QrcodeVue class="mx-auto" :value="qrCode" :size="200" max-width="25rem" />
</div> </div>
<VAlert <VAlert :title="secret" variant="tonal" type="warning" class="my-4" :text="t('profile.secretKeyTip')">
:title="secret"
variant="tonal"
type="warning"
class="my-4"
text="如果您在使用二维码时遇到困难,请在您的应用程序中选择手动输入以上代码。"
>
<template #prepend /> <template #prepend />
</VAlert> </VAlert>
<VForm> <VForm>
<VTextField <VTextField
v-model="otpPassword" v-model="otpPassword"
type="text" type="text"
label="输入验证码以确认开启双重验证" :label="t('profile.enterVerificationCode')"
autocomplete="" autocomplete=""
class="mb-8" class="mb-8"
variant="outlined" variant="outlined"
/> />
<div class="d-flex justify-end flex-wrap gap-4"> <div class="d-flex justify-end flex-wrap gap-4">
<VBtn variant="outlined" color="secondary" @click="otpDialog = false"> 取消 </VBtn> <VBtn variant="outlined" color="secondary" @click="otpDialog = false"> {{ t('common.cancel') }} </VBtn>
<VBtn @click="judgeOtpPassword"> <VBtn @click="judgeOtpPassword">
<template #prepend> <template #prepend>
<VIcon icon="mdi-check" /> <VIcon icon="mdi-check" />
</template> </template>
确定 {{ t('common.confirm') }}
</VBtn> </VBtn>
</div> </div>
</VForm> </VForm>
+2 -2
View File
@@ -3,8 +3,8 @@
"baseUrl": "./", "baseUrl": "./",
"target": "esnext", "target": "esnext",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"module": "esnext", "module": "Node16",
"moduleResolution": "node", "moduleResolution": "node16",
"isolatedModules": true, "isolatedModules": true,
"strict": true, "strict": true,
"jsx": "preserve", "jsx": "preserve",
+2504 -2232
View File
File diff suppressed because it is too large Load Diff