feat: 添加语言管理功能,更新系统设置页面,优化用户体验

This commit is contained in:
2025-12-20 02:23:55 +07:00
parent 2703e6d007
commit 916cbe9d24
10 changed files with 210 additions and 16 deletions

5
auto-imports.d.ts vendored
View File

@@ -213,6 +213,7 @@ declare global {
const useInterval: typeof import('@vueuse/core').useInterval
const useIntervalFn: typeof import('@vueuse/core').useIntervalFn
const useKeyModifier: typeof import('@vueuse/core').useKeyModifier
const useLanguage: typeof import('./src/composables/useLanguage').useLanguage
const useLastChanged: typeof import('@vueuse/core').useLastChanged
const useLink: typeof import('vue-router').useLink
const useLocalStorage: typeof import('@vueuse/core').useLocalStorage
@@ -333,6 +334,9 @@ declare global {
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
// @ts-ignore
export type { Language } from './src/composables/useLanguage'
import('./src/composables/useLanguage')
// @ts-ignore
export type { QRScanResult } from './src/composables/useQRScanner'
import('./src/composables/useQRScanner')
// @ts-ignore
@@ -557,6 +561,7 @@ declare module 'vue' {
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLanguage: UnwrapRef<typeof import('./src/composables/useLanguage')['useLanguage']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>

View File

@@ -1,10 +1,15 @@
<script setup lang="ts">
const { initializeWallet } = useWalletStore();
const { locale } = useLanguage();
onMounted(() => {
initializeWallet();
console.log("App mounted successfully");
});
watch(locale, (newLocale) => {
document.querySelector("html")?.setAttribute("lang", newLocale);
}, { immediate: true });
</script>
<template>

View File

@@ -0,0 +1,60 @@
import type { Locale } from "vue-i18n";
export interface Language {
code: Locale;
name: string;
nativeName: string;
}
/**
* 语言管理组合式函数
*/
export function useLanguage() {
const { locale, availableLocales } = useI18n();
// 可用的语言列表
const languages: Language[] = [
{
code: "zh-CN",
name: "Chinese (Simplified)",
nativeName: "简体中文",
},
{
code: "en-US",
name: "English (US)",
nativeName: "English",
},
];
// 当前语言
const currentLanguage = computed(() => {
return languages.find(lang => lang.code === locale.value) || languages[0];
});
/**
* 切换语言
*/
function setLanguage(langCode: Locale) {
locale.value = langCode;
// 持久化到 localStorage
localStorage.setItem("app-language", langCode);
}
/**
* 从 localStorage 加载保存的语言
*/
function loadSavedLanguage() {
const saved = localStorage.getItem("app-language") as Locale;
if (saved && availableLocales.includes(saved)) {
locale.value = saved;
}
}
return {
languages,
currentLanguage,
locale,
setLanguage,
loadSavedLanguage,
};
}

View File

@@ -299,5 +299,22 @@
"title": "Verify your email",
"description": "We have sent a verification code to {email}. Please enter the code below to verify your email address."
}
},
"settings": {
"title": "Settings",
"language": "Language",
"aboutUs": "About Us",
"clearCache": "Clear Cache",
"checkUpdate": "Check for Updates",
"cacheCleared": "Cache cleared",
"updateAvailable": "New version available",
"currentVersion": "Current version",
"latestVersion": "Latest version",
"newVersion": "New version",
"cancel": "Cancel",
"updateNow": "Update Now",
"alreadyLatest": "Already up to date",
"checkUpdateFailed": "Failed to check for updates",
"languageTitle": "Language / 语言"
}
}

View File

@@ -299,5 +299,22 @@
"title": "验证您的邮箱",
"description": "我们已向 {email} 发送了一个验证码。请在下方输入代码以验证您的邮箱地址。"
}
},
"settings": {
"title": "设置",
"language": "语言",
"aboutUs": "关于我们",
"clearCache": "清除缓存",
"checkUpdate": "检查更新",
"cacheCleared": "缓存已清除",
"updateAvailable": "发现新版本",
"currentVersion": "当前版本",
"latestVersion": "最新版本",
"newVersion": "新版本",
"cancel": "取消",
"updateNow": "立即更新",
"alreadyLatest": "已是最新版本",
"checkUpdateFailed": "检查更新失败",
"languageTitle": "语言 / Language"
}
}

View File

@@ -1,3 +1,5 @@
import type { WritableComputedRef } from "vue";
import type { Locale } from "vue-i18n";
import { IonicVue } from "@ionic/vue";
import { createPinia } from "pinia";
import { createApp } from "vue";

View File

@@ -55,8 +55,18 @@ const routes: Array<RouteRecordRaw> = [
},
{
path: "/system-settings",
component: () => import("@/views/system-settings/outlet.vue"),
children: [
{
path: "",
component: () => import("@/views/system-settings/index.vue"),
},
{
path: "language",
component: () => import("@/views/system-settings/language.vue"),
},
],
},
{
path: "/issue/issuing-apply",
props: ({ query, params }) => ({ query, params }),

View File

@@ -1,15 +1,18 @@
<script lang="ts" setup>
import { alertController, toastController } from "@ionic/vue";
import { alertController, toastController, useIonRouter } from "@ionic/vue";
import { checkbox, close, information, languageOutline, refresh } from "ionicons/icons";
const { t } = useI18n();
const router = useIonRouter();
const { cacheSize, calculateCacheSize, clearCache } = useCacheSize();
const { isChecking, checkForUpdate } = useAppUpdate();
const { currentLanguage } = useLanguage();
function handleClearCache() {
clearCache();
calculateCacheSize();
toastController.create({
message: "缓存已清除",
message: t("settings.cacheCleared"),
duration: 2000,
icon: checkbox,
position: "bottom",
@@ -23,15 +26,15 @@ async function handleCheckUpdate() {
if (result.hasUpdate) {
const alert = await alertController.create({
header: "发现新版本",
message: `当前版本: ${result.currentVersion}\n最新版本: ${result.latestVersion || "新版本"}`,
header: t("settings.updateAvailable"),
message: `${t("settings.currentVersion")}: ${result.currentVersion}\n${t("settings.latestVersion")}: ${result.latestVersion || t("settings.newVersion")}`,
buttons: [
{
text: "取消",
text: t("settings.cancel"),
role: "cancel",
},
{
text: "立即更新",
text: t("settings.updateNow"),
handler: () => {
window.location.reload();
},
@@ -42,7 +45,7 @@ async function handleCheckUpdate() {
}
else {
const toast = await toastController.create({
message: "已是最新版本",
message: t("settings.alreadyLatest"),
duration: 2000,
icon: checkbox,
position: "bottom",
@@ -53,7 +56,7 @@ async function handleCheckUpdate() {
}
catch (error) {
const toast = await toastController.create({
message: "检查更新失败",
message: t("settings.checkUpdateFailed"),
duration: 2000,
position: "bottom",
color: "danger",
@@ -78,20 +81,20 @@ onMounted(() => {
<ion-content :fullscreen="true" class="ion-padding">
<ion-list lines="full">
<ion-list-header>
<ion-label>设置</ion-label>
<ion-label>{{ t("settings.title") }}</ion-label>
</ion-list-header>
<ion-item button>
<ion-item button @click="router.push('/system-settings/language')">
<div class="flex justify-between w-full items-center">
<div class="flex-center space-x-2">
<div class="icon">
<ion-icon :icon="languageOutline" class="text-lg" />
</div>
<div class="text-sm font-semibold">
语言
{{ t("settings.language") }}
</div>
</div>
<div class="end">
简体中文
{{ currentLanguage.nativeName }}
</div>
</div>
</ion-item>
@@ -102,7 +105,7 @@ onMounted(() => {
<ion-icon :icon="information" class="text-lg" />
</div>
<div class="text-sm font-semibold">
关于我们
{{ t("settings.aboutUs") }}
</div>
</div>
</div>
@@ -114,7 +117,7 @@ onMounted(() => {
<ion-icon :icon="close" class="text-lg" />
</div>
<div class="text-sm font-semibold">
清除缓存
{{ t("settings.clearCache") }}
</div>
</div>
<div class="end">
@@ -129,7 +132,7 @@ onMounted(() => {
<ion-icon :icon="refresh" class="text-lg" :class="{ 'animate-spin': isChecking }" />
</div>
<div class="text-sm font-semibold">
检查更新
{{ t("settings.checkUpdate") }}
</div>
</div>
</div>

View File

@@ -0,0 +1,58 @@
<script lang="ts" setup>
import { useIonRouter } from "@ionic/vue";
const { t } = useI18n();
const router = useIonRouter();
const { languages, locale, setLanguage } = useLanguage();
function handleLanguageChange(event: CustomEvent) {
const langCode = event.detail.value;
setLanguage(langCode);
// 返回上一页
setTimeout(() => {
router.back();
}, 300);
}
</script>
<template>
<ion-page>
<ion-header>
<ion-toolbar class="ui-toolbar">
<ion-back-button slot="start" />
<ion-title>{{ t("settings.languageTitle") }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true" class="ion-padding">
<ion-list lines="full">
<ion-radio-group :value="locale" @ion-change="handleLanguageChange">
<ion-item v-for="lang in languages" :key="lang.code">
<ion-radio :value="lang.code">
<div class="language-item">
<div class="font-semibold">
{{ lang.nativeName }}
</div>
<div class="text-xs text-gray-500">
{{ lang.name }}
</div>
</div>
</ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>
</ion-content>
</ion-page>
</template>
<style lang='css' scoped></style>
"css" scoped>
@reference "tailwindcss";
.language-item {
@apply py-1;
}
ion-radio {
width: 100%;
}

View File

@@ -0,0 +1,17 @@
<script lang="ts" setup></script>
<template>
<ion-page>
<ion-header>
<ion-toolbar class="ui-toolbar">
<ion-back-button slot="start" />
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true" class="ion-padding">
<ion-router-outlet />
</ion-content>
</ion-page>
</template>
<style lang='css' scoped></style>