更新国际化支持:在多个对话框组件中引入 vue-i18n,优化文本翻译,确保多语言显示的一致性和准确性。

This commit is contained in:
jxxghp
2025-04-28 08:55:52 +08:00
parent daf70b6da4
commit af7aa7d47b
12 changed files with 825 additions and 148 deletions

View File

@@ -1,5 +1,9 @@
<script lang="ts" setup>
import api from '@/api'
import { useI18n } from 'vue-i18n'
// 多语言支持
const { t } = useI18n()
// 定义输入
const props = defineProps({
@@ -30,22 +34,32 @@ async function savaAlistConfig() {
<template>
<VDialog width="50rem" scrollable max-height="85vh">
<VCard title="AList配置" class="rounded-t">
<VCard :title="t('dialog.alistConfig.title')" class="rounded-t">
<VDialogCloseBtn @click="emit('close')" />
<VCardText>
<VRow>
<VCol cols="12">
<VTextField v-model="props.conf.url" hint="AList服务地址" label="地址" persistent-hint />
<VTextField
v-model="props.conf.url"
:hint="t('dialog.alistConfig.serverUrl')"
:label="t('dialog.alistConfig.serverUrl')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VTextField v-model="props.conf.username" hint="AList登录用户名" label="用户名" persistent-hint />
<VTextField
v-model="props.conf.username"
:hint="t('dialog.alistConfig.username')"
:label="t('dialog.alistConfig.username')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
type="password"
v-model="props.conf.password"
hint="AList登录密码"
label="密码"
:hint="t('dialog.alistConfig.password')"
:label="t('dialog.alistConfig.password')"
persistent-hint
/>
</VCol>
@@ -53,7 +67,9 @@ async function savaAlistConfig() {
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
{{ t('dialog.alistConfig.complete') }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>

View File

@@ -1,5 +1,9 @@
<script lang="ts" setup>
import api from '@/api'
import { useI18n } from 'vue-i18n'
// 多语言支持
const { t } = useI18n()
// 定义输入
defineProps({
@@ -16,7 +20,7 @@ const emit = defineEmits(['done', 'close'])
const qrCodeUrl = ref('')
// 下方的提示信息
const text = ref('请用阿里云盘 App 扫码')
const text = ref(t('dialog.aliyunAuth.scanQrCode'))
// 提醒类型
const alertType = ref<'success' | 'info' | 'error' | 'warning' | undefined>('info')
@@ -85,7 +89,7 @@ onUnmounted(() => {
<template>
<VDialog width="40rem" scrollable max-height="85vh">
<VCard title="阿里云盘登录" class="rounded-t">
<VCard :title="t('dialog.aliyunAuth.loginTitle')" class="rounded-t">
<VDialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2 flex flex-col items-center">
<div class="my-6 rounded text-center p-3 border">
@@ -103,7 +107,9 @@ onUnmounted(() => {
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
{{ t('dialog.aliyunAuth.complete') }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>

View File

@@ -1,5 +1,9 @@
<script lang="ts" setup>
import api from '@/api'
import { useI18n } from 'vue-i18n'
// 多语言支持
const { t } = useI18n()
// 定义输入
const props = defineProps({
@@ -14,7 +18,7 @@ if (!props.conf.filepath) {
}
if (!props.conf.content) {
props.conf.content = '# 请在此处填写rclone配置文件内容 \n# 请参考 https://rclone.org/docs/ \n# 存储节点名必须为MP'
props.conf.content = t('dialog.rcloneConfig.defaultContent')
}
// 定义事件
@@ -38,12 +42,12 @@ async function savaRcloneConfig() {
<template>
<VDialog width="50rem" scrollable max-height="85vh">
<VCard title="RClone配置" class="rounded-t">
<VCard :title="t('dialog.rcloneConfig.title')" class="rounded-t">
<VDialogCloseBtn @click="emit('close')" />
<VCardText>
<VRow>
<VCol cols="12">
<VTextField v-model="props.conf.filepath" label="rclone配置文件路径" />
<VTextField v-model="props.conf.filepath" :label="t('dialog.rcloneConfig.filePath')" />
</VCol>
<VCol cols="12">
<VAceEditor
@@ -59,7 +63,9 @@ async function savaRcloneConfig() {
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">
{{ t('dialog.rcloneConfig.complete') }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>

View File

@@ -4,6 +4,10 @@ import api from '@/api'
import { useDisplay, useTheme } from 'vuetify'
import { formatFileSize } from '@/@core/utils/formatters'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import { useI18n } from 'vue-i18n'
// 多语言支持
const { t } = useI18n()
// 显示器宽度
const display = useDisplay()
@@ -36,11 +40,11 @@ const siteData = computed(() => siteDatas.value[siteDatas.value.length - 1])
const historySeries = computed(() => {
return [
{
name: '上传量',
name: t('dialog.siteUserData.uploadTitle'),
data: siteDatas.value.map(item => Math.round((item.upload ?? 0) / 1024 / 1024 / 1024)),
},
{
name: '下载量',
name: t('dialog.siteUserData.downloadTitle'),
data: siteDatas.value.map(item => Math.round((item.download ?? 0) / 1024 / 1024 / 1024)),
},
]
@@ -135,7 +139,7 @@ const historyChartOptions = computed(() => {
const seedingSeries = computed(() => {
return [
{
name: '体积',
name: t('dialog.siteUserData.volumeTitle'),
data: siteData.value?.seeding_info?.map(item => [item[0] ?? 0, Math.round((item[1] ?? 0) / 1024 / 1024 / 1024)]),
},
]
@@ -162,7 +166,7 @@ const seedingChartOptions = computed(() => {
enabled: true,
x: {
formatter: function (val: number) {
return '数量:' + val.toLocaleString()
return t('dialog.siteUserData.countTitle') + val.toLocaleString()
},
},
style: {
@@ -188,7 +192,7 @@ const seedingChartOptions = computed(() => {
},
},
title: {
text: '数量',
text: t('dialog.siteUserData.countTitle'),
},
tickAmount: 10,
},
@@ -282,7 +286,7 @@ onBeforeMount(async () => {
<VCard class="rounded-t">
<VCardItem>
<VCardTitle
>{{ `数据 - ${props.site?.name}` }}
>{{ t('dialog.siteUserData.title') }} - {{ props.site?.name }}
<IconBtn @click.stop="refreshSiteData" color="info"><VIcon icon="mdi-refresh" /></IconBtn>
</VCardTitle>
<VDialogCloseBtn @click="emit('close')" />
@@ -296,9 +300,9 @@ onBeforeMount(async () => {
<VCardText class="d-flex align-center">
<div class="d-flex justify-space-between" style="inline-size: 100%">
<div class="d-flex flex-column gap-y-1 overflow-hidden">
<span class="text-base">用户等级</span>
<span class="text-base">{{ t('dialog.siteUserData.userLevel') }}</span>
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
{{ siteData?.user_level || '无' }}
{{ siteData?.user_level || t('dialog.siteUserData.noData') }}
</h5>
</div>
<VAvatar variant="tonal" size="42" rounded>
@@ -314,7 +318,7 @@ onBeforeMount(async () => {
<VCardText class="d-flex align-center">
<div class="d-flex justify-space-between" style="inline-size: 100%">
<div class="d-flex flex-column gap-y-1 overflow-hidden">
<span class="text-base">积分</span>
<span class="text-base">{{ t('dialog.siteUserData.bonus') }}</span>
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
{{ siteData?.bonus?.toLocaleString() }}
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.bonus)">
@@ -335,7 +339,7 @@ onBeforeMount(async () => {
<VCardText class="d-flex align-center">
<div class="d-flex justify-space-between" style="inline-size: 100%">
<div class="d-flex flex-column gap-y-1">
<span class="text-base">分享率</span>
<span class="text-base">{{ t('dialog.siteUserData.ratio') }}</span>
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
{{ siteData?.ratio }}
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.ratio)">
@@ -356,7 +360,7 @@ onBeforeMount(async () => {
<VCardText class="d-flex align-center">
<div class="d-flex justify-space-between" style="inline-size: 100%">
<div class="d-flex flex-column gap-y-1 overflow-hidden">
<span class="text-base">总上传量</span>
<span class="text-base">{{ t('dialog.siteUserData.uploadTotal') }}</span>
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
{{ formatFileSize(siteData?.upload || 0) }}
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.upload)">
@@ -377,7 +381,7 @@ onBeforeMount(async () => {
<VCardText class="d-flex align-center">
<div class="d-flex justify-space-between" style="inline-size: 100%">
<div class="d-flex flex-column gap-y-1 overflow-hidden">
<span class="text-base">总下载量</span>
<span class="text-base">{{ t('dialog.siteUserData.downloadTotal') }}</span>
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
{{ formatFileSize(siteData?.download || 0) }}
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.download)">
@@ -398,7 +402,7 @@ onBeforeMount(async () => {
<VCardText class="d-flex align-center">
<div class="d-flex justify-space-between" style="inline-size: 100%">
<div class="d-flex flex-column gap-y-1 overflow-hidden">
<span class="text-base">总做种数</span>
<span class="text-base">{{ t('dialog.siteUserData.seedingCount') }}</span>
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
{{ siteData?.seeding?.toLocaleString() }}
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.seeding)">
@@ -419,7 +423,7 @@ onBeforeMount(async () => {
<VCardText class="d-flex align-center">
<div class="d-flex justify-space-between" style="inline-size: 100%">
<div class="d-flex flex-column gap-y-1 overflow-hidden">
<span class="text-base">总做种体积</span>
<span class="text-base">{{ t('dialog.siteUserData.seedingSize') }}</span>
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
{{ formatFileSize(siteData?.seeding_size || 0) }}
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.seeding_size)">
@@ -440,7 +444,7 @@ onBeforeMount(async () => {
<VCardText class="d-flex align-center">
<div class="d-flex justify-space-between" style="inline-size: 100%">
<div class="d-flex flex-column gap-y-1 overflow-hidden">
<span class="text-base">加入时间</span>
<span class="text-base">{{ t('dialog.siteUserData.joinTime') }}</span>
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
{{ siteData?.join_at?.split(' ')[0] }}
</h5>
@@ -455,7 +459,7 @@ onBeforeMount(async () => {
</VRow>
<VRow>
<VCol>
<VCard title="历史流量">
<VCard :title="t('dialog.siteUserData.trafficHistory')">
<VCardText>
<VApexChart type="line" :options="historyChartOptions" :series="historySeries" :height="300" />
</VCardText>
@@ -464,7 +468,7 @@ onBeforeMount(async () => {
</VRow>
<VRow>
<VCol>
<VCard title="做种分布">
<VCard :title="t('dialog.siteUserData.seedingDistribution')">
<VCardText>
<VApexChart type="scatter" :options="seedingChartOptions" :series="seedingSeries" :height="300" />
</VCardText>
@@ -474,6 +478,6 @@ onBeforeMount(async () => {
</VCardText>
</VCard>
<!-- 进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" text="正在刷新站点数据..." />
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="t('dialog.siteUserData.refreshing')" />
</VDialog>
</template>

View File

@@ -5,6 +5,10 @@ import api from '@/api'
import type { DownloaderConf, FilterRuleGroup, Site, Subscribe, TransferDirectoryConf } from '@/api/types'
import { useDisplay } from 'vuetify'
import { useConfirm } from 'vuetify-use-dialog'
import { useI18n } from 'vue-i18n'
// i18n
const { t } = useI18n()
// 显示器宽度
const display = useDisplay()
@@ -80,7 +84,7 @@ const episodeGroupOptions = computed(() => {
// 生成1到100季的下拉框选项
const seasonItems = ref(
Array.from({ length: 101 }, (_, i) => i).map(item => ({
title: `${item}`,
title: t('dialog.subscribeEdit.seasonFormat', { number: item }),
value: item,
})),
)
@@ -106,7 +110,7 @@ async function loadDownloaderSetting() {
try {
const downloaders: DownloaderConf[] = await api.get('download/clients')
downloaderOptions.value = [
{ title: '默认', value: '' },
{ title: t('common.default'), value: '' },
...downloaders.map((item: { name: any }) => ({
title: item.name,
value: item.name,
@@ -229,8 +233,8 @@ async function getSubscribeInfo() {
// 删除订阅
async function removeSubscribe() {
const isConfirmed = await createConfirm({
title: '确认',
content: `是否确认取消订阅?`,
title: t('common.confirm'),
content: t('dialog.subscribeEdit.cancelSubscribeConfirm'),
})
if (!isConfirmed) return
@@ -268,7 +272,7 @@ const targetDirectories = computed(() => {
// 质量选择框数据
const qualityOptions = ref([
{
title: '全部',
title: t('common.all'),
value: '',
},
{
@@ -308,7 +312,7 @@ const qualityOptions = ref([
// 分辨率选择框数据
const resolutionOptions = ref([
{
title: '全部',
title: t('common.all'),
value: '',
},
{
@@ -328,7 +332,7 @@ const resolutionOptions = ref([
// 特效选择框数据
const effectOptions = ref([
{
title: '全部',
title: t('common.all'),
value: '',
},
{
@@ -362,11 +366,16 @@ onMounted(() => {
<template>
<VDialog scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="`${
:title="
props.default
? `${props.type}默认订阅规则`
: `编辑订阅 - ${subscribeForm.name} ${subscribeForm.season ? `第 ${subscribeForm.season} 季` : ''}`
}`"
? t('dialog.subscribeEdit.titleDefault')
: t('dialog.subscribeEdit.titleEditFormat', {
name: subscribeForm.name,
season: subscribeForm.season
? t('dialog.subscribeEdit.seasonFormat', { number: subscribeForm.season })
: '',
})
"
class="rounded-t"
>
<VCardText>
@@ -374,10 +383,10 @@ onMounted(() => {
<VForm @submit.prevent="() => {}">
<VTabs v-model="activeTab" show-arrows>
<VTab value="basic">
<div>基础</div>
<div>{{ t('dialog.subscribeEdit.tabs.basic') }}</div>
</VTab>
<VTab value="advance">
<div>进阶</div>
<div>{{ t('dialog.subscribeEdit.tabs.advance') }}</div>
</VTab>
</VTabs>
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
@@ -387,26 +396,26 @@ onMounted(() => {
<VCol cols="12" md="4">
<VTextField
v-model="subscribeForm.keyword"
label="搜索关键词"
hint="指定搜索站点时使用的关键词"
:label="t('dialog.subscribeEdit.searchKeyword')"
:hint="t('dialog.subscribeEdit.searchKeywordHint')"
persistent-hint
/>
</VCol>
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="4">
<VTextField
v-model="subscribeForm.total_episode"
label="总集数"
:label="t('dialog.subscribeEdit.totalEpisode')"
:rules="[numberValidator]"
hint="剧集总集数"
:hint="t('dialog.subscribeEdit.totalEpisodeHint')"
persistent-hint
/>
</VCol>
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="4">
<VTextField
v-model="subscribeForm.start_episode"
label="开始集数"
:label="t('dialog.subscribeEdit.startEpisode')"
:rules="[numberValidator]"
hint="开始订阅集数"
:hint="t('dialog.subscribeEdit.startEpisodeHint')"
persistent-hint
/>
</VCol>
@@ -415,27 +424,27 @@ onMounted(() => {
<VCol cols="12" md="4">
<VSelect
v-model="subscribeForm.quality"
label="质量"
:label="t('dialog.subscribeEdit.quality')"
:items="qualityOptions"
hint="订阅资源质量"
:hint="t('dialog.subscribeEdit.qualityHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VSelect
v-model="subscribeForm.resolution"
label="分辨率"
:label="t('dialog.subscribeEdit.resolution')"
:items="resolutionOptions"
hint="订阅资源分辨率"
:hint="t('dialog.subscribeEdit.resolutionHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VSelect
v-model="subscribeForm.effect"
label="特效"
:label="t('dialog.subscribeEdit.effect')"
:items="effectOptions"
hint="订阅资源特效"
:hint="t('dialog.subscribeEdit.effectHint')"
persistent-hint
/>
</VCol>
@@ -446,10 +455,10 @@ onMounted(() => {
v-model="subscribeForm.sites"
:items="selectSitesOptions"
chips
label="订阅站点"
:label="t('dialog.subscribeEdit.subscribeSites')"
multiple
clearable
hint="订阅的站点范围,不选使用系统设置"
:hint="t('dialog.subscribeEdit.subscribeSitesHint')"
persistent-hint
/>
</VCol>
@@ -459,8 +468,8 @@ onMounted(() => {
<VSelect
v-model="subscribeForm.downloader"
:items="downloaderOptions"
label="下载器"
hint="指定该订阅使用的下载器"
:label="t('dialog.subscribeEdit.downloader')"
:hint="t('dialog.subscribeEdit.downloaderHint')"
persistent-hint
/>
</VCol>
@@ -468,8 +477,8 @@ onMounted(() => {
<VCombobox
v-model="subscribeForm.save_path"
:items="targetDirectories"
label="保存路径"
hint="指定该订阅的下载保存路径,留空自动使用设定的下载目录"
:label="t('dialog.subscribeEdit.savePath')"
:hint="t('dialog.subscribeEdit.savePathHint')"
persistent-hint
/>
</VCol>
@@ -478,24 +487,24 @@ onMounted(() => {
<VCol cols="12" md="4">
<VSwitch
v-model="subscribeForm.best_version"
label="洗版"
hint="根据洗版优先级进行洗版订阅"
:label="t('dialog.subscribeEdit.bestVersion')"
:hint="t('dialog.subscribeEdit.bestVersionHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="4">
<VSwitch
v-model="subscribeForm.search_imdbid"
label="使用 ImdbID 搜索"
hint="开使用 ImdbID 精确搜索资源"
:label="t('dialog.subscribeEdit.searchImdbid')"
:hint="t('dialog.subscribeEdit.searchImdbidHint')"
persistent-hint
/>
</VCol>
<VCol v-if="props.default" cols="12" md="4">
<VSwitch
v-model="subscribeForm.show_edit_dialog"
label="订阅时编辑更多规则"
hint="添加订阅时显示此编辑订阅对话框"
:label="t('dialog.subscribeEdit.showEditDialog')"
:hint="t('dialog.subscribeEdit.showEditDialogHint')"
persistent-hint
/>
</VCol>
@@ -508,16 +517,16 @@ onMounted(() => {
<VCol cols="12" md="6">
<VTextField
v-model="subscribeForm.include"
label="包含(关键字、正则式)"
hint="包含规则,支持正则表达式"
:label="t('dialog.subscribeEdit.include')"
:hint="t('dialog.subscribeEdit.includeHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="subscribeForm.exclude"
label="排除(关键字、正则式)"
hint="排除规则,支持正则表达式"
:label="t('dialog.subscribeEdit.exclude')"
:hint="t('dialog.subscribeEdit.excludeHint')"
persistent-hint
/>
</VCol>
@@ -530,8 +539,8 @@ onMounted(() => {
chips
multiple
clearable
label="优先级规则组"
hint="按选定的过滤规则组对订阅进行过滤"
:label="t('dialog.subscribeEdit.filterGroups')"
:hint="t('dialog.subscribeEdit.filterGroupsHint')"
persistent-hint
/>
</VCol>
@@ -540,8 +549,8 @@ onMounted(() => {
v-model="subscribeForm.episode_group"
:items="episodeGroupOptions"
:item-props="episodeGroupItemProps"
label="指定剧集组"
hint="按特定剧集组识别和刮削"
:label="t('dialog.subscribeEdit.episodeGroup')"
:hint="t('dialog.subscribeEdit.episodeGroupHint')"
persistent-hint
/>
</VCol>
@@ -549,16 +558,16 @@ onMounted(() => {
<VSelect
v-model="subscribeForm.season"
:items="seasonItems"
label="指定季"
hint="指定任意季订阅"
:label="t('dialog.subscribeEdit.season')"
:hint="t('dialog.subscribeEdit.seasonHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" v-if="!props.default">
<VTextField
v-model="subscribeForm.media_category"
label="自定义类别"
hint="指定类别名称,留空自动识别"
:label="t('dialog.subscribeEdit.mediaCategory')"
:hint="t('dialog.subscribeEdit.mediaCategoryHint')"
persistent-hint
/>
</VCol>
@@ -567,14 +576,10 @@ onMounted(() => {
<VCol cols="12">
<VTextarea
v-model="subscribeForm.custom_words"
label="自定义识别词"
hint="只对该订阅使用的识别词"
:label="t('dialog.subscribeEdit.customWords')"
:hint="t('dialog.subscribeEdit.customWordsHint')"
persistent-hint
placeholder="屏蔽词
被替换词 => 替换词
前定位词 <> 后定位词 >> 集偏移量EP
被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量EP
其中替换词支持格式:{[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]} 直接指定TMDBID/豆瓣ID识别其中s、e为季数和集数可选"
:placeholder="t('dialog.subscribeEdit.customWordsPlaceholder')"
/>
</VCol>
</VRow>
@@ -585,7 +590,7 @@ onMounted(() => {
</VCardText>
<VCardActions class="pt-3">
<VBtn v-if="!props.default" color="error" @click="removeSubscribe" variant="outlined" class="me-3">
取消订阅
{{ t('dialog.subscribeEdit.cancelSubscribe') }}
</VBtn>
<VSpacer />
<VBtn
@@ -594,7 +599,7 @@ onMounted(() => {
prepend-icon="mdi-content-save"
class="px-5"
>
保存
{{ t('dialog.subscribeEdit.save') }}
</VBtn>
</VCardActions>
</VCard>

View File

@@ -2,6 +2,10 @@
import api from '@/api'
import { SubscrbieInfo } from '@/api/types'
import { useDisplay } from 'vuetify'
import { useI18n } from 'vue-i18n'
// i18n
const { t } = useI18n()
// 显示器宽度
const display = useDisplay()
@@ -21,15 +25,15 @@ const subScribeInfo = ref<SubscrbieInfo>()
// 下载文件表头
const downloadHeaders = [
{ title: '集', key: 'episode_number', sortable: true },
{ title: '种子', key: 'torrent_title', sortable: true },
{ title: '文件', key: 'file_path', sortable: true },
{ title: t('dialog.subscribeFiles.episodeColumn'), key: 'episode_number', sortable: true },
{ title: t('dialog.subscribeFiles.torrentColumn'), key: 'torrent_title', sortable: true },
{ title: t('dialog.subscribeFiles.fileColumn'), key: 'file_path', sortable: true },
]
// 媒体库文件表头
const libraryHeaders = [
{ title: '集', key: 'episode_number', sortable: true },
{ title: '文件', key: 'file_path', sortable: true },
{ title: t('dialog.subscribeFiles.episodeColumn'), key: 'episode_number', sortable: true },
{ title: t('dialog.subscribeFiles.fileColumn'), key: 'file_path', sortable: true },
]
// 调用API查询订阅文件信息
@@ -102,7 +106,7 @@ onBeforeMount(() => {
{{ subScribeInfo?.subscribe?.name }}
</div>
<div v-if="subScribeInfo?.subscribe?.season" class="text-lg align-self-center align-self-lg-end ms-3">
{{ subScribeInfo?.subscribe?.season }}
{{ t('dialog.subscribeFiles.season', { number: subScribeInfo?.subscribe?.season }) }}
</div>
</h1>
<div>{{ subScribeInfo?.subscribe?.year }}</div>
@@ -119,13 +123,13 @@ onBeforeMount(() => {
<VTab value="download" selected-class="v-slide-group-item--active v-tab--selected">
<div>
<VIcon size="20" start icon="mdi-download" />
下载文件
{{ t('dialog.subscribeFiles.downloadTab') }}
</div>
</VTab>
<VTab value="library" selected-class="v-slide-group-item--active v-tab--selected">
<div>
<VIcon size="20" start icon="mdi-filmstrip-box-multiple" />
媒体库文件
{{ t('dialog.subscribeFiles.libraryTab') }}
</div>
</VTab>
</VTabs>
@@ -143,9 +147,9 @@ onBeforeMount(() => {
return-object
fixed-header
hover
items-per-page-text="每页条数"
page-text="{0}-{1} {2} "
loading-text="加载中..."
:items-per-page-text="t('dialog.subscribeFiles.itemsPerPage')"
:page-text="t('dialog.subscribeFiles.pageText')"
:loading-text="t('dialog.subscribeFiles.loadingText')"
>
<template #item.episode_number="{ item }">
<div class="text-high-emphasis pt-1">{{ item.episode_number }}. {{ item.title }}</div>
@@ -158,7 +162,7 @@ onBeforeMount(() => {
<template #item.file_path="{ item }">
<div class="text-xs" v-for="file in item.download">{{ file.file_path }}</div>
</template>
<template #no-data> 没有数据 </template>
<template #no-data> {{ t('dialog.subscribeFiles.noData') }} </template>
</VDataTable>
</div>
</transition>
@@ -176,9 +180,9 @@ onBeforeMount(() => {
return-object
fixed-header
hover
items-per-page-text="每页条数"
page-text="{0}-{1} {2} "
loading-text="加载中..."
:items-per-page-text="t('dialog.subscribeFiles.itemsPerPage')"
:page-text="t('dialog.subscribeFiles.pageText')"
:loading-text="t('dialog.subscribeFiles.loadingText')"
>
<template #item.episode_number="{ item }">
<div class="text-high-emphasis pt-1">{{ item.episode_number }}. {{ item.title }}</div>
@@ -186,7 +190,7 @@ onBeforeMount(() => {
<template #item.file_path="{ item }">
<div class="text-xs" v-for="file in item.library">{{ file.file_path }}</div>
</template>
<template #no-data> 没有数据 </template>
<template #no-data> {{ t('dialog.subscribeFiles.noData') }} </template>
</VDataTable>
</div>
</transition>

View File

@@ -3,6 +3,10 @@ import { formatFileSize } from '@/@core/utils/formatters'
import api from '@/api'
import { FileItem, TransferQueue } from '@/api/types'
import { useDisplay } from 'vuetify'
import { useI18n } from 'vue-i18n'
// 多语言支持
const { t } = useI18n()
// 显示器宽度
const display = useDisplay()
@@ -16,7 +20,7 @@ const dataList = ref<TransferQueue[]>([])
const progressEventSource = ref<EventSource>()
// 整理进度文本
const progressText = ref('请稍候 ...')
const progressText = ref(t('dialog.transferQueue.processing'))
// 整理进度
const progressValue = ref(0)
@@ -29,10 +33,11 @@ const activeTab = ref('')
// 状态标签
const stateDict: { [key: string]: string } = {
'waiting': '等待中',
'running': '正在整理',
'completed': '完成',
'failed': '失败',
'waiting': t('dialog.transferQueue.waitingState'),
'running': t('dialog.transferQueue.runningState'),
'completed': t('dialog.transferQueue.finishedState'),
'failed': t('dialog.transferQueue.failedState'),
'cancelled': t('dialog.transferQueue.cancelledState'),
}
// 获取状态颜色
@@ -88,13 +93,13 @@ async function remove_queue_task(fileitem: FileItem) {
// 使用SSE监听加载进度
function startLoadingProgress() {
progressText.value = '请稍候 ...'
progressText.value = t('dialog.transferQueue.processing')
progressEventSource.value = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer`)
progressEventSource.value.onmessage = event => {
const progress = JSON.parse(event.data)
if (progress) {
if (!progress.enable) {
progressText.value = '请稍候 ...'
progressText.value = t('dialog.transferQueue.processing')
progressValue.value = 0
if (refreshFlag.value) {
refreshFlag.value = false
@@ -138,7 +143,7 @@ onUnmounted(() => {
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
<VCard class="mx-auto" width="100%">
<VCardItem>
<VCardTitle>整理队列</VCardTitle>
<VCardTitle>{{ t('dialog.transferQueue.title') }}</VCardTitle>
</VCardItem>
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
@@ -151,7 +156,7 @@ onUnmounted(() => {
<VCardItem v-if="dataList.length > 0 && progressValue > 0" class="text-center pt-2">
<span class="text-sm">{{ progressText }}</span>
</VCardItem>
<VCardText v-if="dataList.length === 0" class="text-center"> 没有正在整理的任务 </VCardText>
<VCardText v-if="dataList.length === 0" class="text-center"> {{ t('dialog.transferQueue.noTasks') }} </VCardText>
<VCardText>
<VTabs v-model="activeTab" show-arrows class="v-tabs-pill" stacked>
<VTab
@@ -169,7 +174,7 @@ onUnmounted(() => {
<VListItem v-for="task in activeTasks">
<VListItemTitle>{{ task.fileitem.name }}</VListItemTitle>
<VListItemSubtitle>
大小{{ formatFileSize(task.fileitem.size || 0) }}
{{ t('dialog.transferQueue.sizeTitle') }}{{ formatFileSize(task.fileitem.size || 0) }}
<VChip size="small" :color="getStateColor(task.state)" class="ms-2">
{{ stateDict[task.state] }}
</VChip>

View File

@@ -2,6 +2,10 @@
import { isNullOrEmptyObject } from '@/@core/utils'
import api from '@/api'
import { useToast } from 'vue-toast-notification'
import { useI18n } from 'vue-i18n'
// 多语言支持
const { t } = useI18n()
// 定义事件
const emit = defineEmits(['done', 'close'])
@@ -89,17 +93,17 @@ async function handleDone() {
// 认证处理
async function checkUser() {
if (!authForm.value.site) {
$toast.error('请选择认证站点!')
$toast.error(t('dialog.userAuth.selectSiteRequired'))
return
}
if (!authSites.value[authForm.value.site]) {
$toast.error('站点配置不存在!')
$toast.error(t('dialog.userAuth.siteConfigNotExist'))
return
}
if (formFields.value.length > 0) {
for (const field of formFields.value) {
if (!authForm.value.params[field.site.toUpperCase() + '_' + field.key.toUpperCase()]) {
$toast.error(`请输入${field.name}`)
$toast.error(t('dialog.userAuth.fieldRequired', { name: field.name }))
return
}
}
@@ -108,13 +112,13 @@ async function checkUser() {
try {
const result: { [key: string]: any } = await api.post(`site/auth`, authForm.value)
if (result.success) {
$toast.success('用户认证成功,请重新登录!')
$toast.success(t('dialog.userAuth.authSuccess'))
// 1秒后刷新页面
setTimeout(() => {
emit('done')
}, 1000)
} else {
$toast.error(`认证失败:${result.message}`)
$toast.error(t('dialog.userAuth.authFailed', { message: result.message }))
}
} catch (e) {
console.error(e)
@@ -130,7 +134,7 @@ onMounted(async () => {
<template>
<VDialog width="40rem" max-height="85vh">
<VCard title="用户认证" class="rounded-t">
<VCard :title="t('dialog.userAuth.title')" class="rounded-t">
<VDialogCloseBtn @click="emit('close')" />
<VCardText>
<VRow>
@@ -140,7 +144,7 @@ onMounted(async () => {
:items="dropdownItems"
item-value="key"
item-title="name"
label="选择认证站点"
:label="t('dialog.userAuth.selectSite')"
item-props
>
</VSelect>
@@ -169,7 +173,7 @@ onMounted(async () => {
size="large"
:disabled="loading"
>
开始认证
{{ t('dialog.userAuth.authBtn') }}
</VBtn>
</VCardText>
</VCard>

View File

@@ -9,6 +9,10 @@ import api from '@/api'
import WorkflowSidebar from '@/layouts/components/WorkflowSidebar.vue'
import DropzoneBackground from '@/layouts/components/DropzoneBackground.vue'
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
import { useI18n } from 'vue-i18n'
// 多语言支持
const { t } = useI18n()
const { onConnect, addEdges, nodes, edges, addNodes, screenToFlowCoordinate } = useVueFlow()
@@ -18,7 +22,7 @@ const { onDragOver, onDrop, onDragLeave, isDragOver } = useDragAndDrop()
onConnect((connection: Connection) => {
// 双重校验
if (!isValidConnection(connection)) {
$toast.warning('非法连接:不能连接自身或同类型端口!')
$toast.warning(t('dialog.workflowActions.invalidConnection'))
return
}
addEdges(connection)
@@ -67,7 +71,7 @@ const loadComponent = async (componentName: string) => {
if (component) {
return ((await component()) as any).default
}
throw new Error(`组件 ${componentName} 未找到`)
throw new Error(t('dialog.workflowActions.componentNotFound', { component: componentName }))
}
// 将所有components中的组件加载到nodeTypes中
@@ -132,7 +136,7 @@ function handleComponentClick(action: any) {
addNodes(newNode)
// 显示提示
$toast.success('已添加组件到画布')
$toast.success(t('dialog.workflowActions.componentAdded'))
}
// 调用API 编辑任务
@@ -144,10 +148,10 @@ async function updateWorkflow() {
try {
const result: { [key: string]: string } = await api.put(`workflow/${workflowForm.value.id}`, workflowForm.value)
if (result.success) {
$toast.success(`保存任务流程成功!`)
$toast.success(t('dialog.workflowActions.saveSuccess'))
emit('save')
} else {
$toast.error(`保存任务流程失败:${result.message}`)
$toast.error(t('dialog.workflowActions.saveFailed', { message: result.message }))
}
} catch (error) {
console.error(error)
@@ -164,10 +168,10 @@ function saveCodeString(type: string, code: any) {
edges.value = codeObject.flows || []
}
importCodeDialog.value = false
$toast.success('导入成功!')
$toast.success(t('dialog.workflowActions.importSuccess'))
}
} catch (error) {
$toast.error('导入失败!')
$toast.error(t('dialog.workflowActions.importFailed'))
console.error(error)
}
}
@@ -176,7 +180,7 @@ function saveCodeString(type: string, code: any) {
function shareWorkflow() {
const codeString = JSON.stringify({ actions: nodes.value, flows: edges.value })
navigator.clipboard.writeText(codeString)
$toast.success('任务流程代码已复制到剪贴板!')
$toast.success(t('dialog.workflowActions.codeCopied'))
}
onMounted(() => {
@@ -202,7 +206,7 @@ const isMacOS = computed(() => {
<VIcon size="large" color="white" icon="mdi-close" />
</VBtn>
</VToolbarItems>
<VToolbarTitle> 编辑流程 - {{ workflow?.name }} </VToolbarTitle>
<VToolbarTitle> {{ t('dialog.workflowActions.title') }} - {{ workflow?.name }} </VToolbarTitle>
<VSpacer></VSpacer>
<VToolbarItems>
<VBtn icon variant="text" @click="importCodeDialog = true" class="ms-2">
@@ -248,7 +252,7 @@ const isMacOS = computed(() => {
<ImportCodeDialog
v-if="importCodeDialog"
v-model="importCodeDialog"
title="导入任务流程"
:title="t('dialog.workflowActions.importTitle')"
dataType="workflow"
@close="importCodeDialog = false"
@save="saveCodeString"

View File

@@ -5,6 +5,10 @@ import { doneNProgress, startNProgress } from '@/api/nprogress'
import { requiredValidator } from '@/@validators'
import api from '@/api'
import { useDisplay } from 'vuetify'
import { useI18n } from 'vue-i18n'
// 多语言支持
const { t } = useI18n()
// 输入参数
const props = defineProps({
@@ -13,7 +17,9 @@ const props = defineProps({
})
// 新增或修改字样
const title = computed(() => (props.workflow ? '编辑' : '创建'))
const title = computed(() =>
props.workflow ? t('dialog.workflowAddEdit.editTitle') : t('dialog.workflowAddEdit.addTitle'),
)
// 显示器宽度
const display = useDisplay()
@@ -38,17 +44,17 @@ const $toast = useToast()
// 调用API 新增任务
async function addWorkflow() {
if (!workflowForm.value.name || !workflowForm.value.timer) {
$toast.error('请填写完整信息!')
$toast.error(t('dialog.workflowAddEdit.nameRequired'))
return
}
startNProgress()
try {
const result: { [key: string]: string } = await api.post('workflow/', workflowForm.value)
if (result.success) {
$toast.success(`创建任务成功,请编辑流程!`)
$toast.success(t('dialog.workflowAddEdit.addSuccess'))
emit('save')
} else {
$toast.error(`创建任务失败:${result.message}`)
$toast.error(t('dialog.workflowAddEdit.addFailed', { message: result.message }))
}
} catch (error) {
console.error(error)
@@ -59,17 +65,17 @@ async function addWorkflow() {
// 调用API 编辑任务
async function editWorkflow() {
if (!workflowForm.value.name || !workflowForm.value.timer) {
$toast.error('请填写完整信息!')
$toast.error(t('dialog.workflowAddEdit.nameRequired'))
return
}
startNProgress()
try {
const result: { [key: string]: string } = await api.put(`workflow/${workflowForm.value.id}`, workflowForm.value)
if (result.success) {
$toast.success(`修改任务成功!`)
$toast.success(t('dialog.workflowAddEdit.editSuccess'))
emit('save')
} else {
$toast.error(`修改任务失败:${result.message}`)
$toast.error(t('dialog.workflowAddEdit.editFailed', { message: result.message }))
}
} catch (error) {
console.error(error)
@@ -80,7 +86,7 @@ async function editWorkflow() {
<template>
<VDialog scrollable :close-on-back="false" eager max-width="30rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="`${title}任务`" class="rounded-t">
<VCard :title="title" class="rounded-t">
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText>
@@ -89,24 +95,28 @@ async function editWorkflow() {
<VCol cols="12">
<VTextField
v-model="workflowForm.name"
label="别名"
:label="t('dialog.workflowAddEdit.name')"
:rules="[requiredValidator]"
persistent-hint
hint="任务名称"
:hint="t('dialog.workflowAddEdit.namePlaceholder')"
/>
</VCol>
<VCol cols="12">
<VCronField
v-model="workflowForm.timer"
label="定时"
:label="t('dialog.workflowAddEdit.schedule')"
:rules="[requiredValidator]"
placeholder="5位cron表达式"
persistent-hint
hint="任务执行周期"
:hint="t('dialog.workflowAddEdit.cronExprDesc')"
/>
</VCol>
<VCol cols="12">
<VTextarea v-model="workflowForm.description" label="任务描述" />
<VTextarea
v-model="workflowForm.description"
:label="t('dialog.workflowAddEdit.desc')"
:placeholder="t('dialog.workflowAddEdit.descPlaceholder')"
/>
</VCol>
</VRow>
</VForm>
@@ -122,10 +132,10 @@ async function editWorkflow() {
prepend-icon="mdi-content-save"
class="px-5"
>
保存
{{ t('dialog.workflowAddEdit.confirm') }}
</VBtn>
<VBtn v-else block color="primary" variant="elevated" @click="addWorkflow" prepend-icon="mdi-plus" class="px-5">
创建
{{ t('dialog.workflowAddEdit.confirm') }}
</VBtn>
</VCardActions>
</VCard>