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