mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-31 21:30:33 +08:00
更新国际化支持:在多个对话框组件中引入 vue-i18n,优化文本翻译,确保多语言显示的一致性和准确性。
This commit is contained in:
@@ -5,6 +5,10 @@ import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import type { DownloaderConf, MediaInfo, TorrentInfo, TransferDirectoryConf } from '@/api/types'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import { VCardTitle, VChip } from 'vuetify/lib/components/index.mjs'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -38,7 +42,9 @@ const loading = ref(false)
|
||||
const icon = computed(() => (loading.value ? 'mdi-progress-download' : 'mdi-download'))
|
||||
|
||||
// 计算按钮文字
|
||||
const buttonText = computed(() => (loading.value ? '下载中...' : '开始下载'))
|
||||
const buttonText = computed(() =>
|
||||
loading.value ? t('dialog.addDownload.downloading') : t('dialog.addDownload.startDownload'),
|
||||
)
|
||||
|
||||
// 加载目录设置
|
||||
async function loadDirectories() {
|
||||
@@ -96,12 +102,20 @@ async function addDownload() {
|
||||
|
||||
if (result && result.success) {
|
||||
// 添加下载成功
|
||||
$toast.success(`${props.torrent?.site_name} ${props.torrent?.title} 下载成功!`)
|
||||
$toast.success(
|
||||
t('dialog.addDownload.downloadSuccess', { site: props.torrent?.site_name, title: props.torrent?.title }),
|
||||
)
|
||||
// 下载成功,返回链接
|
||||
emit('done', props.torrent?.enclosure)
|
||||
} else {
|
||||
// 添加下载失败
|
||||
$toast.error(`${props.torrent?.site_name} ${props.torrent?.title} 下载失败:${result?.message}!`)
|
||||
$toast.error(
|
||||
t('dialog.addDownload.downloadFailed', {
|
||||
site: props.torrent?.site_name,
|
||||
title: props.torrent?.title,
|
||||
message: result?.message,
|
||||
}),
|
||||
)
|
||||
// 下载失败,返回错误原因
|
||||
emit('error', result?.message)
|
||||
}
|
||||
@@ -123,7 +137,7 @@ onMounted(() => {
|
||||
<VCardTitle class="py-4 me-12">
|
||||
<VIcon icon="mdi-download" class="me-2" />
|
||||
<span v-if="title">{{ torrent?.site_name }} - {{ title }}</span>
|
||||
<span v-else>确认下载</span>
|
||||
<span v-else>{{ t('dialog.addDownload.confirmDownload') }}</span>
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VDivider />
|
||||
@@ -165,9 +179,9 @@ onMounted(() => {
|
||||
v-model="selectedDownloader"
|
||||
:items="downloaderOptions"
|
||||
size="small"
|
||||
label="下载器(默认)"
|
||||
:label="t('dialog.addDownload.downloader')"
|
||||
variant="underlined"
|
||||
placeholder="留空默认"
|
||||
:placeholder="t('dialog.addDownload.defaultPlaceholder')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
@@ -175,9 +189,9 @@ onMounted(() => {
|
||||
<VCombobox
|
||||
v-model="selectedDirectory"
|
||||
:items="targetDirectories"
|
||||
label="保存目录(自动)"
|
||||
:label="t('dialog.addDownload.saveDirectory')"
|
||||
size="small"
|
||||
placeholder="留空自动匹配"
|
||||
:placeholder="t('dialog.addDownload.autoPlaceholder')"
|
||||
variant="underlined"
|
||||
density="compact"
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
@@ -27,7 +32,9 @@ function handleImport() {
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="elevated" @click="handleImport" prepend-icon="mdi-import" class="px-5 me-3"> 导入 </VBtn>
|
||||
<VBtn variant="elevated" @click="handleImport" prepend-icon="mdi-import" class="px-5 me-3">
|
||||
{{ t('dialog.importCode.import') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { Context } from '@/api/types'
|
||||
import MediaInfoCard from '../cards/MediaInfoCard.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
|
||||
// 输入参数
|
||||
defineProps({
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
value: Number,
|
||||
text: String,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<!-- 手动整理进度框 -->
|
||||
<!-- Progress Dialog -->
|
||||
<VDialog :scrim="false" width="25rem">
|
||||
<VCard elevation="3" color="primary">
|
||||
<VCardText class="text-center">
|
||||
{{ props.text }}
|
||||
{{ props.text || t('dialog.progress.processing') }}
|
||||
<VProgressLinear color="white" class="mb-0 mt-1" :model-value="props.value" indeterminate />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
@@ -5,6 +5,10 @@ import { getNavMenus, getSettingTabs } from '@/router/i18n-menu'
|
||||
import { NavMenu } from '@/@layouts/types'
|
||||
import { useUserStore } from '@/stores'
|
||||
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
|
||||
// 定义props,接收modelValue
|
||||
const props = defineProps<{
|
||||
@@ -89,7 +93,7 @@ function getMenus(): NavMenu[] {
|
||||
item =>
|
||||
item &&
|
||||
menus.push({
|
||||
title: '设定 -> ' + item.title,
|
||||
title: t('setting') + ' -> ' + item.title,
|
||||
icon: item.icon,
|
||||
to: `/setting?tab=${item.tab}`,
|
||||
header: '',
|
||||
@@ -311,7 +315,7 @@ onMounted(() => {
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
class="search-input"
|
||||
placeholder="输入关键词搜索..."
|
||||
:placeholder="t('dialog.searchBar.searchPlaceholder')"
|
||||
@keydown.enter="searchMedia('media')"
|
||||
hide-details
|
||||
clearable
|
||||
@@ -330,7 +334,9 @@ onMounted(() => {
|
||||
<!-- 有搜索词时显示结果 -->
|
||||
<VList lines="two" v-if="searchWord" class="search-list py-2">
|
||||
<!-- 搜索结果分组标题 -->
|
||||
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6"> 媒体 </VListSubheader>
|
||||
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6">
|
||||
{{ t('media.movie') }}
|
||||
</VListSubheader>
|
||||
|
||||
<!-- 媒体搜索选项 -->
|
||||
<VHover>
|
||||
@@ -352,9 +358,12 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<VListItemTitle class="font-weight-medium"> 电影、电视剧 </VListItemTitle>
|
||||
<VListItemTitle class="font-weight-medium"
|
||||
>{{ t('recommend.categoryMovie') }}、{{ t('recommend.categoryTV') }}</VListItemTitle
|
||||
>
|
||||
<VListItemSubtitle class="text-body-2 text-medium-emphasis mt-1">
|
||||
搜索 <span class="primary-text font-weight-medium">{{ searchWord }}</span> 相关的影视作品
|
||||
{{ t('common.search') }} <span class="primary-text font-weight-medium">{{ searchWord }}</span>
|
||||
{{ t('resource.title') }}
|
||||
</VListItemSubtitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="mdi-chevron-right" color="primary" />
|
||||
@@ -382,9 +391,10 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<VListItemTitle class="font-weight-medium"> 系列合集 </VListItemTitle>
|
||||
<VListItemTitle class="font-weight-medium">{{ t('dialog.searchBar.collections') }}</VListItemTitle>
|
||||
<VListItemSubtitle class="text-body-2 text-medium-emphasis mt-1">
|
||||
搜索 <span class="primary-text font-weight-medium">{{ searchWord }}</span> 相关的系列作品
|
||||
{{ t('common.search') }} <span class="primary-text font-weight-medium">{{ searchWord }}</span>
|
||||
{{ t('dialog.searchBar.collectionSearch') }}
|
||||
</VListItemSubtitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="mdi-chevron-right" color="primary" />
|
||||
@@ -412,9 +422,10 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<VListItemTitle class="font-weight-medium"> 演职人员 </VListItemTitle>
|
||||
<VListItemTitle class="font-weight-medium">{{ t('browse.actor') }}</VListItemTitle>
|
||||
<VListItemSubtitle class="text-body-2 text-medium-emphasis mt-1">
|
||||
搜索 <span class="primary-text font-weight-medium">{{ searchWord }}</span> 相关的演员、导演等
|
||||
{{ t('common.search') }} <span class="primary-text font-weight-medium">{{ searchWord }}</span>
|
||||
{{ t('dialog.searchBar.actorSearch') }}
|
||||
</VListItemSubtitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="mdi-chevron-right" color="primary" />
|
||||
@@ -438,9 +449,10 @@ onMounted(() => {
|
||||
<VIcon icon="mdi-history" :color="hover.isHovering ? 'primary' : 'medium-emphasis'" size="small" />
|
||||
</div>
|
||||
</template>
|
||||
<VListItemTitle class="font-weight-medium"> 整理记录 </VListItemTitle>
|
||||
<VListItemTitle class="font-weight-medium">{{ t('navItems.history') }}</VListItemTitle>
|
||||
<VListItemSubtitle class="text-body-2 text-medium-emphasis mt-1">
|
||||
搜索 <span class="primary-text font-weight-medium">{{ searchWord }}</span> 相关的历史记录
|
||||
{{ t('common.search') }} <span class="primary-text font-weight-medium">{{ searchWord }}</span>
|
||||
{{ t('dialog.searchBar.historySearch') }}
|
||||
</VListItemSubtitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="mdi-chevron-right" color="primary" />
|
||||
@@ -452,7 +464,9 @@ onMounted(() => {
|
||||
<!-- 其他搜索结果 -->
|
||||
<template v-if="matchedSubscribeItems.length > 0">
|
||||
<VDivider class="mx-4 mx-sm-6 my-2 search-divider" />
|
||||
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6"> 订阅 </VListSubheader>
|
||||
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6">{{
|
||||
t('dialog.searchBar.subscriptions')
|
||||
}}</VListSubheader>
|
||||
|
||||
<VHover v-for="subscribe in matchedSubscribeItems" :key="subscribe.id">
|
||||
<template #default="hover">
|
||||
@@ -475,7 +489,9 @@ onMounted(() => {
|
||||
</template>
|
||||
<VListItemTitle class="font-weight-medium">
|
||||
{{ subscribe.name
|
||||
}}<span v-if="subscribe.season" class="text-body-2"> 第 {{ subscribe.season }} 季</span>
|
||||
}}<span v-if="subscribe.season" class="text-body-2">
|
||||
{{ t('resource.season') }} {{ subscribe.season }}</span
|
||||
>
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle class="text-body-2 text-medium-emphasis mt-1">
|
||||
{{ subscribe.type }}
|
||||
@@ -490,7 +506,9 @@ onMounted(() => {
|
||||
|
||||
<template v-if="matchedMenuItems.length > 0">
|
||||
<VDivider class="mx-4 mx-sm-6 my-2 search-divider" />
|
||||
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6"> 功能 </VListSubheader>
|
||||
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6">{{
|
||||
t('dialog.searchBar.functions')
|
||||
}}</VListSubheader>
|
||||
|
||||
<VHover v-for="menu in matchedMenuItems" :key="menu.title">
|
||||
<template #default="hover">
|
||||
@@ -527,7 +545,9 @@ onMounted(() => {
|
||||
|
||||
<template v-if="matchedPluginItems.length > 0">
|
||||
<VDivider class="mx-4 mx-sm-6 my-2 search-divider" />
|
||||
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6"> 插件 </VListSubheader>
|
||||
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6">{{
|
||||
t('dialog.searchBar.plugins')
|
||||
}}</VListSubheader>
|
||||
|
||||
<VHover v-for="plugin in matchedPluginItems" :key="plugin.id">
|
||||
<template #default="hover">
|
||||
@@ -561,7 +581,9 @@ onMounted(() => {
|
||||
<!-- 将站点资源搜索移到最底部 -->
|
||||
<template v-if="searchWord">
|
||||
<VDivider class="mx-4 mx-sm-6 my-2 search-divider" />
|
||||
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6"> 站点资源 </VListSubheader>
|
||||
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6">{{
|
||||
t('dialog.searchBar.siteResources')
|
||||
}}</VListSubheader>
|
||||
|
||||
<VCard class="mx-3 mx-sm-6 mb-4 mt-2 site-search-card">
|
||||
<VCardText class="pa-3 pa-sm-4">
|
||||
@@ -571,9 +593,10 @@ onMounted(() => {
|
||||
<VIcon icon="mdi-file-search" color="primary" size="small" />
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="font-weight-medium text-body-1">在站点中搜索种子资源</div>
|
||||
<div class="font-weight-medium text-body-1">{{ t('dialog.searchBar.searchInSites') }}</div>
|
||||
<div class="text-caption text-medium-emphasis mt-1">
|
||||
搜索 <span class="primary-text font-weight-medium">{{ searchWord }}</span> 相关资源
|
||||
{{ t('common.search') }} <span class="primary-text font-weight-medium">{{ searchWord }}</span>
|
||||
{{ t('dialog.searchBar.relatedResources') }}
|
||||
</div>
|
||||
</div>
|
||||
<VBtn
|
||||
@@ -584,7 +607,7 @@ onMounted(() => {
|
||||
variant="flat"
|
||||
class="search-btn"
|
||||
>
|
||||
搜索
|
||||
{{ t('common.search') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
@@ -628,7 +651,7 @@ onMounted(() => {
|
||||
class="ml-auto site-select-btn"
|
||||
rounded="pill"
|
||||
>
|
||||
选择站点
|
||||
{{ t('dialog.searchBar.selectSites') }}
|
||||
<VIcon size="small" class="ml-1">mdi-cog-outline</VIcon>
|
||||
</VBtn>
|
||||
</div>
|
||||
@@ -641,7 +664,7 @@ onMounted(() => {
|
||||
<!-- 无搜索词时显示最近搜索和提示 -->
|
||||
<div v-else class="recent-searches py-6 px-4 px-sm-6">
|
||||
<div v-if="recentSearches.length > 0" class="mb-6">
|
||||
<div class="text-h6 font-weight-medium mb-3">最近搜索</div>
|
||||
<div class="text-h6 font-weight-medium mb-3">{{ t('dialog.searchBar.recentSearches') }}</div>
|
||||
<div class="d-flex flex-wrap">
|
||||
<VChip
|
||||
v-for="(word, index) in recentSearches"
|
||||
@@ -658,12 +681,12 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-6 py-6 empty-search-state">
|
||||
<div v-else class="text-center mt-6 py-6 empty-search-state">
|
||||
<div class="search-icon-wrapper mx-auto mb-4">
|
||||
<VIcon icon="mdi-magnify" size="large" color="primary" />
|
||||
</div>
|
||||
<div class="text-h6 font-weight-medium mb-2">输入关键词开始搜索</div>
|
||||
<div class="text-body-2 text-medium-emphasis">可搜索电影、电视剧、演员、资源等</div>
|
||||
<div class="text-h6 font-weight-medium mb-2">{{ t('dialog.searchBar.searchPlaceholder') }}</div>
|
||||
<div class="text-body-2 text-medium-emphasis">{{ t('dialog.searchBar.searchTip') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
@@ -790,10 +813,10 @@ onMounted(() => {
|
||||
|
||||
.empty-search-state,
|
||||
.empty-site-state {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
animation: fade-in 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { Site } from '@/api/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
sites: {
|
||||
@@ -29,7 +33,9 @@ watch(
|
||||
|
||||
// 全选/全不选按钮文字
|
||||
const checkAllText = computed(() => {
|
||||
return selectedSites.value.length < props.sites?.length ? '选择全部' : '取消全选'
|
||||
return selectedSites.value.length < props.sites?.length
|
||||
? t('dialog.searchSite.selectAll')
|
||||
: t('dialog.searchSite.deselectAll')
|
||||
})
|
||||
|
||||
// 全选/全不选
|
||||
@@ -49,15 +55,15 @@ const filteredSites = computed(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<!-- 手动整理进度框 -->
|
||||
<!-- Site Selection Dialog -->
|
||||
<VDialog max-width="40rem" fullscreen-mobile>
|
||||
<VCard class="site-dialog">
|
||||
<VCardTitle class="d-flex align-center pa-4">
|
||||
<span class="text-h6 font-weight-medium">选择搜索站点</span>
|
||||
<span class="text-h6 font-weight-medium">{{ t('dialog.searchSite.selectSites') }}</span>
|
||||
<VSpacer />
|
||||
<VTextField
|
||||
v-model="siteFilter"
|
||||
placeholder="过滤站点..."
|
||||
:placeholder="t('dialog.searchSite.siteSearch')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
@@ -91,7 +97,7 @@ const filteredSites = computed(() => {
|
||||
class="text-body-2 font-weight-medium"
|
||||
:class="selectedSites.length > 0 ? 'text-primary' : 'text-medium-emphasis'"
|
||||
>
|
||||
已选择 {{ selectedSites.length }}/{{ sites.length }} 个站点
|
||||
{{ t('dialog.searchSite.searchAllSites', { selected: selectedSites.length, total: sites.length }) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -137,9 +143,9 @@ const filteredSites = computed(() => {
|
||||
<div class="search-icon-wrapper mb-4 mx-auto warning">
|
||||
<VIcon icon="mdi-alert-circle-outline" size="large" color="warning" />
|
||||
</div>
|
||||
<div class="text-h6 font-weight-medium mb-2">没有找到匹配的站点</div>
|
||||
<div class="text-h6 font-weight-medium mb-2">{{ t('torrent.noMatchingResults') }}</div>
|
||||
<div class="text-subtitle-1 text-medium-emphasis mb-4">
|
||||
{{ siteFilter ? '请尝试修改过滤条件' : '站点数据加载失败,请刷新页面重试' }}
|
||||
{{ siteFilter ? t('site.noFilterData') : t('site.sitesWillBeShownHere') }}
|
||||
</div>
|
||||
<VBtn
|
||||
v-if="siteFilter"
|
||||
@@ -149,10 +155,10 @@ const filteredSites = computed(() => {
|
||||
prepend-icon="mdi-refresh"
|
||||
@click="siteFilter = ''"
|
||||
>
|
||||
重置
|
||||
{{ t('torrent.clearFilters') }}
|
||||
</VBtn>
|
||||
<VBtn v-else color="primary" variant="flat" class="mt-3" prepend-icon="mdi-refresh" @click="emit('reload')">
|
||||
重新加载站点
|
||||
{{ t('common.loading') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
@@ -167,7 +173,7 @@ const filteredSites = computed(() => {
|
||||
@click="emit('close')"
|
||||
class="mr-2 d-flex align-center justify-center"
|
||||
>
|
||||
取消
|
||||
{{ t('dialog.searchSite.cancel') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="primary"
|
||||
@@ -177,7 +183,7 @@ const filteredSites = computed(() => {
|
||||
prepend-icon="mdi-magnify"
|
||||
class="d-flex align-center justify-center px-5"
|
||||
>
|
||||
搜索
|
||||
{{ t('dialog.searchSite.confirm') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
|
||||
@@ -5,6 +5,10 @@ import api from '@/api'
|
||||
import type { Subscribe, SubscribeShare } from '@/api/types'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { formatSeason } from '@/@core/utils/formatters'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
@@ -35,11 +39,11 @@ async function doShare() {
|
||||
shareDoing.value = false
|
||||
// 提示
|
||||
if (result.success) {
|
||||
$toast.success(`${props.sub?.name} 分享成功!`)
|
||||
$toast.success(t('dialog.subscribeShare.shareSuccess', { name: props.sub?.name }))
|
||||
// 通知父组件刷新
|
||||
emit('close')
|
||||
} else {
|
||||
$toast.error(`${props.sub?.name} 分享失败:${result.message}!`)
|
||||
$toast.error(t('dialog.subscribeShare.shareFailed', { name: props.sub?.name, message: result.message }))
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
@@ -53,7 +57,9 @@ const $toast = useToast()
|
||||
<template>
|
||||
<VDialog scrollable max-width="30rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard
|
||||
:title="`分享订阅 - ${props.sub?.name} ${props.sub?.season ? `第 ${props.sub?.season} 季` : ''}`"
|
||||
:title="`${t('dialog.subscribeShare.shareSubscription')} - ${props.sub?.name} ${
|
||||
props.sub?.season ? t('dialog.subscribeShare.season', { number: props.sub?.season }) : ''
|
||||
}`"
|
||||
class="rounded-t"
|
||||
>
|
||||
<VCardText>
|
||||
@@ -64,7 +70,7 @@ const $toast = useToast()
|
||||
<VTextField
|
||||
v-model="shareForm.share_title"
|
||||
readonly
|
||||
label="标题"
|
||||
:label="t('dialog.subscribeShare.title')"
|
||||
:rules="[requiredValidator]"
|
||||
persistent-hint
|
||||
/>
|
||||
@@ -72,18 +78,18 @@ const $toast = useToast()
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="shareForm.share_comment"
|
||||
label="说明"
|
||||
:label="t('dialog.subscribeShare.description')"
|
||||
:rules="[requiredValidator]"
|
||||
hint="填写关于该订阅的说明,订阅中的搜索词、识别词等将会默认包含在分享中"
|
||||
:hint="t('dialog.subscribeShare.descriptionHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="shareForm.share_user"
|
||||
label="分享用户"
|
||||
:label="t('dialog.subscribeShare.shareUser')"
|
||||
:rules="[requiredValidator]"
|
||||
hint="分享人的昵称"
|
||||
:hint="t('dialog.subscribeShare.shareUserHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
@@ -100,7 +106,7 @@ const $toast = useToast()
|
||||
class="px-5"
|
||||
:loading="shareDoing"
|
||||
>
|
||||
确认分享
|
||||
{{ t('dialog.subscribeShare.confirmShare') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
|
||||
// 定义输入
|
||||
const props = defineProps({
|
||||
@@ -17,7 +21,7 @@ const emit = defineEmits(['done', 'close'])
|
||||
const qrCodeContent = ref('')
|
||||
|
||||
// 下方的提示信息
|
||||
const text = ref('请使用微信或115客户端扫码')
|
||||
const text = ref(t('dialog.u115Auth.scanQrCode'))
|
||||
|
||||
// 提醒类型
|
||||
const alertType = ref<'success' | 'info' | 'error' | 'warning' | undefined>('info')
|
||||
@@ -61,7 +65,7 @@ async function checkQrcode() {
|
||||
} else if (status == 1) {
|
||||
// 已扫码
|
||||
alertType.value = 'info'
|
||||
text.value = '已扫码,请确认登录'
|
||||
text.value = t('dialog.u115Auth.scanned')
|
||||
clearTimeout(timeoutTimer)
|
||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||
} else if (status == 2) {
|
||||
@@ -92,7 +96,7 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<VDialog width="40rem" scrollable max-height="85vh">
|
||||
<VCard title="115网盘登录" class="rounded-t">
|
||||
<VCard :title="t('dialog.u115Auth.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">
|
||||
@@ -104,7 +108,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.u115Auth.complete') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
@@ -6,6 +6,10 @@ import api from '@/api'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
@@ -52,8 +56,8 @@ const $toast = useToast()
|
||||
|
||||
// 状态下拉项
|
||||
const statusItems = [
|
||||
{ title: '激活', value: 1 },
|
||||
{ title: '已停用', value: 0 },
|
||||
{ title: t('dialog.userAddEdit.active'), value: 1 },
|
||||
{ title: t('dialog.userAddEdit.inactive'), value: 0 },
|
||||
]
|
||||
|
||||
// 扩展User类型以包含note字段
|
||||
@@ -92,19 +96,19 @@ function changeAvatar(file: Event) {
|
||||
const maxSize = 800 * 1024
|
||||
// 检查文件是否为图片
|
||||
if (!allowedTypes.includes(selectedFile.type)) {
|
||||
$toast.error('上传的文件不符合要求,请重新选择头像')
|
||||
$toast.error(t('dialog.userAddEdit.invalidFile'))
|
||||
return
|
||||
}
|
||||
// 检查文件大小
|
||||
if (selectedFile.size > maxSize) {
|
||||
$toast.error('文件大小不得大于800KB')
|
||||
$toast.error(t('dialog.userAddEdit.fileSizeLimit'))
|
||||
return
|
||||
}
|
||||
fileReader.readAsDataURL(selectedFile)
|
||||
fileReader.onload = () => {
|
||||
if (typeof fileReader.result === 'string') {
|
||||
currentAvatar.value = fileReader.result
|
||||
$toast.success('新头像上传成功,待保存后生效!')
|
||||
$toast.success(t('dialog.userAddEdit.avatarUploadSuccess'))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,13 +117,13 @@ function changeAvatar(file: Event) {
|
||||
// 重置默认头像
|
||||
function resetDefaultAvatar() {
|
||||
currentAvatar.value = avatar1
|
||||
$toast.success('已重置为默认头像,待保存后生效!')
|
||||
$toast.success(t('dialog.userAddEdit.resetAvatarSuccess'))
|
||||
}
|
||||
|
||||
// 还原当前头像
|
||||
function restoreCurrentAvatar() {
|
||||
currentAvatar.value = userForm.value.avatar
|
||||
$toast.success('已还原当前使用头像!')
|
||||
$toast.success(t('dialog.userAddEdit.restoreAvatarSuccess'))
|
||||
}
|
||||
|
||||
// 查询用户信息
|
||||
@@ -140,22 +144,22 @@ async function fetchUserInfo() {
|
||||
// 调用API 新增用户
|
||||
async function addUser() {
|
||||
if (isAdding.value) {
|
||||
$toast.error(`正在创建【${userForm.value.name}】用户,请稍后`)
|
||||
$toast.error(t('dialog.userAddEdit.creatingUser', { name: userForm.value.name }))
|
||||
return
|
||||
}
|
||||
if (!currentUserName.value) {
|
||||
$toast.error('用户名不能为空')
|
||||
$toast.error(t('dialog.userAddEdit.usernameRequired'))
|
||||
return
|
||||
} else userForm.value.name = currentUserName.value
|
||||
// 重名检查
|
||||
if (props.usernames && props.usernames.includes(userForm.value.name)) {
|
||||
$toast.error('用户名已存在')
|
||||
$toast.error(t('dialog.userAddEdit.usernameExists'))
|
||||
return
|
||||
}
|
||||
if (!userForm.value?.name || !newPassword.value) return
|
||||
if (newPassword.value || confirmPassword.value) {
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
$toast.error('两次输入的密码不一致')
|
||||
$toast.error(t('dialog.userAddEdit.passwordMismatch'))
|
||||
return
|
||||
}
|
||||
userForm.value.password = newPassword.value
|
||||
@@ -165,10 +169,10 @@ async function addUser() {
|
||||
try {
|
||||
const result: { [key: string]: string } = await api.post('user/', userForm.value)
|
||||
if (result.success) {
|
||||
$toast.success(`用户【${userForm.value.name}】创建成功`)
|
||||
$toast.success(t('dialog.userAddEdit.userCreated', { name: userForm.value.name }))
|
||||
emit('save')
|
||||
} else {
|
||||
$toast.error(`创建用户失败:${result.message}`)
|
||||
$toast.error(t('dialog.userAddEdit.userCreateFailed', { message: result.message }))
|
||||
// 清除用户名
|
||||
userForm.value.name = ''
|
||||
}
|
||||
@@ -182,16 +186,16 @@ async function addUser() {
|
||||
// 调用API更新用户信息
|
||||
async function updateUser() {
|
||||
if (isUpdating.value) {
|
||||
$toast.error(`正在更新【${userForm.value.name}】用户,请稍后`)
|
||||
$toast.error(t('dialog.userAddEdit.updatingUser', { name: userForm.value.name }))
|
||||
return
|
||||
}
|
||||
if (!currentUserName.value) {
|
||||
$toast.error('用户名不能为空')
|
||||
$toast.error(t('dialog.userAddEdit.usernameRequired'))
|
||||
return
|
||||
}
|
||||
if (newPassword.value || confirmPassword.value) {
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
$toast.error('两次输入的密码不一致')
|
||||
$toast.error(t('dialog.userAddEdit.passwordMismatch'))
|
||||
return
|
||||
}
|
||||
userForm.value.password = newPassword.value
|
||||
@@ -219,13 +223,13 @@ async function updateUser() {
|
||||
|
||||
if (result.success) {
|
||||
if (oldUserName !== currentUserName.value) {
|
||||
$toast.success(`【${oldUserName}】更名【${currentUserName.value}】, 更新成功!`)
|
||||
$toast.success(t('dialog.userAddEdit.userUpdateSuccess', { name: `${oldUserName} → ${currentUserName.value}` }))
|
||||
// 如果是当前登录用户,更新当前用户名称显示
|
||||
if (isCurrentUser.value) {
|
||||
userStore.setUserName(currentUserName.value)
|
||||
}
|
||||
} else {
|
||||
$toast.success(`【${userForm.value?.name}】更新成功!`)
|
||||
$toast.success(t('dialog.userAddEdit.userUpdateSuccess', { name: userForm.value?.name }))
|
||||
}
|
||||
// 更新本地头像显示
|
||||
if (oldAvatar !== currentAvatar.value && isCurrentUser.value) {
|
||||
@@ -234,10 +238,10 @@ async function updateUser() {
|
||||
emit('save')
|
||||
} else {
|
||||
if (oldUserName !== currentUserName.value) {
|
||||
$toast.error(`【${oldUserName}】更名【${currentUserName.value}】, 更新失败:${result.message}`)
|
||||
$toast.error(t('dialog.userAddEdit.userUpdateFailed', { message: result.message }))
|
||||
currentUserName.value = oldUserName
|
||||
} else {
|
||||
$toast.error(`【${userForm.value?.name}】更新失败:${result.message}`)
|
||||
$toast.error(t('dialog.userAddEdit.userUpdateFailed', { message: result.message }))
|
||||
}
|
||||
}
|
||||
//失败缓存值还原
|
||||
@@ -247,7 +251,7 @@ async function updateUser() {
|
||||
userForm.value.avatar = oldAvatar
|
||||
userForm.value.password = ''
|
||||
} catch (error) {
|
||||
$toast.error(`【${userForm.value?.name}】更新失败!`)
|
||||
$toast.error(t('dialog.userAddEdit.userUpdateFailed', { message: '' }))
|
||||
console.error('更新失败:', error)
|
||||
}
|
||||
doneNProgress()
|
||||
@@ -288,7 +292,9 @@ onMounted(() => {
|
||||
<template>
|
||||
<VDialog scrollable :close-on-back="false" eager max-width="40rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard
|
||||
:title="`${props.oper === 'add' ? '新增' : '编辑'}用户${props.oper !== 'add' ? ` - ${userName}` : ''}`"
|
||||
:title="`${props.oper === 'add' ? t('dialog.userAddEdit.add') : t('dialog.userAddEdit.edit')}${
|
||||
props.oper !== 'add' ? ` - ${userName}` : ''
|
||||
}`"
|
||||
class="rounded-t"
|
||||
>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
@@ -302,7 +308,7 @@ onMounted(() => {
|
||||
<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('dialog.userAddEdit.uploadAvatar') }}</span>
|
||||
</VBtn>
|
||||
|
||||
<input
|
||||
@@ -316,7 +322,7 @@ onMounted(() => {
|
||||
|
||||
<VBtn type="reset" color="info" variant="tonal" @click="restoreCurrentAvatar" v-if="props.oper !== 'add'">
|
||||
<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.cancel') }}</span>
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
@@ -326,17 +332,17 @@ onMounted(() => {
|
||||
@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('dialog.userAddEdit.resetDefaultAvatar') }}</span>
|
||||
</VBtn>
|
||||
</div>
|
||||
<p class="text-body-1 mb-0">允许 JPG、PNG、GIF、WEBP 格式, 最大尺寸 800KB。</p>
|
||||
<p class="text-body-1 mb-0">{{ t('dialog.userAddEdit.fileSizeLimit') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VDivider class="my-10">
|
||||
<span>用户基础设置</span>
|
||||
<span>{{ t('dialog.userAddEdit.saveUserInfo') }}</span>
|
||||
</VDivider>
|
||||
<VRow>
|
||||
<VCol md="6" cols="12">
|
||||
@@ -344,11 +350,17 @@ onMounted(() => {
|
||||
v-model="currentUserName"
|
||||
density="comfortable"
|
||||
:readonly="props.oper !== 'add'"
|
||||
label="用户名"
|
||||
:label="t('dialog.userAddEdit.username')"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model="userForm.email" density="comfortable" clearable label="邮箱" type="email" />
|
||||
<VTextField
|
||||
v-model="userForm.email"
|
||||
density="comfortable"
|
||||
clearable
|
||||
:label="t('dialog.userAddEdit.email')"
|
||||
type="email"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
@@ -357,7 +369,7 @@ onMounted(() => {
|
||||
:type="isNewPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isNewPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
clearable
|
||||
label="密码"
|
||||
:label="t('dialog.userAddEdit.password')"
|
||||
autocomplete=""
|
||||
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
|
||||
/>
|
||||
@@ -370,7 +382,7 @@ onMounted(() => {
|
||||
:type="isConfirmPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
clearable
|
||||
label="确认密码"
|
||||
:label="t('dialog.userAddEdit.confirmPassword')"
|
||||
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
|
||||
/>
|
||||
</VCol>
|
||||
@@ -379,7 +391,7 @@ onMounted(() => {
|
||||
v-model="userForm.nickname"
|
||||
density="comfortable"
|
||||
clearable
|
||||
label="昵称"
|
||||
:label="t('dialog.userAddEdit.nickname')"
|
||||
placeholder="显示昵称,优先于用户名显示"
|
||||
/>
|
||||
</VCol>
|
||||
@@ -389,35 +401,45 @@ onMounted(() => {
|
||||
:items="statusItems"
|
||||
item-text="title"
|
||||
item-value="value"
|
||||
label="状态"
|
||||
:label="t('dialog.userAddEdit.status')"
|
||||
dense
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VDivider class="my-10">
|
||||
<span>账号绑定</span>
|
||||
<span>{{ t('dialog.userAddEdit.notifications') }}</span>
|
||||
</VDivider>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model="userForm.settings.wechat_userid" density="comfortable" clearable label="微信用户" />
|
||||
<VTextField
|
||||
v-model="userForm.settings.wechat_userid"
|
||||
density="comfortable"
|
||||
clearable
|
||||
:label="t('dialog.userAddEdit.wechat')"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="userForm.settings.telegram_userid"
|
||||
density="comfortable"
|
||||
clearable
|
||||
label="Telegram用户"
|
||||
:label="t('dialog.userAddEdit.telegram')"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model="userForm.settings.slack_userid" density="comfortable" clearable label="Slack用户" />
|
||||
<VTextField
|
||||
v-model="userForm.settings.slack_userid"
|
||||
density="comfortable"
|
||||
clearable
|
||||
:label="t('dialog.userAddEdit.slack')"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="userForm.settings.vocechat_userid"
|
||||
density="comfortable"
|
||||
clearable
|
||||
label="VoceChat用户"
|
||||
:label="t('dialog.userAddEdit.vocechat')"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -425,7 +447,7 @@ onMounted(() => {
|
||||
v-model="userForm.settings.synologychat_userid"
|
||||
density="comfortable"
|
||||
clearable
|
||||
label="SynologyChat用户"
|
||||
:label="t('dialog.userAddEdit.synologyChat')"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -445,8 +467,8 @@ onMounted(() => {
|
||||
prepend-icon="mdi-plus"
|
||||
class="px-5"
|
||||
>
|
||||
<span v-if="isAdding">创建中...</span>
|
||||
<span v-else>创建</span>
|
||||
<span v-if="isAdding">{{ t('common.loading') }}</span>
|
||||
<span v-else>{{ t('common.add') }}</span>
|
||||
</VBtn>
|
||||
<VBtn
|
||||
v-else
|
||||
@@ -457,8 +479,8 @@ onMounted(() => {
|
||||
prepend-icon="mdi-content-save"
|
||||
class="px-5"
|
||||
>
|
||||
<span v-if="isUpdating">更新中...</span>
|
||||
<span v-else>更新</span>
|
||||
<span v-if="isUpdating">{{ t('common.loading') }}</span>
|
||||
<span v-else>{{ t('common.save') }}</span>
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
|
||||
Reference in New Issue
Block a user