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

This commit is contained in:
jxxghp
2025-04-28 08:29:08 +08:00
parent 819dd01d60
commit daf70b6da4
11 changed files with 425 additions and 104 deletions

View File

@@ -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"
/>

View File

@@ -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>

View File

@@ -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({

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">允许 JPGPNGGIFWEBP 格式 最大尺寸 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>