feat: 添加语言管理功能,更新系统设置页面,优化用户体验
This commit is contained in:
5
auto-imports.d.ts
vendored
5
auto-imports.d.ts
vendored
@@ -213,6 +213,7 @@ declare global {
|
|||||||
const useInterval: typeof import('@vueuse/core').useInterval
|
const useInterval: typeof import('@vueuse/core').useInterval
|
||||||
const useIntervalFn: typeof import('@vueuse/core').useIntervalFn
|
const useIntervalFn: typeof import('@vueuse/core').useIntervalFn
|
||||||
const useKeyModifier: typeof import('@vueuse/core').useKeyModifier
|
const useKeyModifier: typeof import('@vueuse/core').useKeyModifier
|
||||||
|
const useLanguage: typeof import('./src/composables/useLanguage').useLanguage
|
||||||
const useLastChanged: typeof import('@vueuse/core').useLastChanged
|
const useLastChanged: typeof import('@vueuse/core').useLastChanged
|
||||||
const useLink: typeof import('vue-router').useLink
|
const useLink: typeof import('vue-router').useLink
|
||||||
const useLocalStorage: typeof import('@vueuse/core').useLocalStorage
|
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'
|
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
import('vue')
|
import('vue')
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
export type { Language } from './src/composables/useLanguage'
|
||||||
|
import('./src/composables/useLanguage')
|
||||||
|
// @ts-ignore
|
||||||
export type { QRScanResult } from './src/composables/useQRScanner'
|
export type { QRScanResult } from './src/composables/useQRScanner'
|
||||||
import('./src/composables/useQRScanner')
|
import('./src/composables/useQRScanner')
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -557,6 +561,7 @@ declare module 'vue' {
|
|||||||
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
|
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
|
||||||
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
|
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
|
||||||
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
|
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 useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
||||||
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
|
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
|
||||||
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
|
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { initializeWallet } = useWalletStore();
|
const { initializeWallet } = useWalletStore();
|
||||||
|
const { locale } = useLanguage();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initializeWallet();
|
initializeWallet();
|
||||||
console.log("App mounted successfully");
|
console.log("App mounted successfully");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(locale, (newLocale) => {
|
||||||
|
document.querySelector("html")?.setAttribute("lang", newLocale);
|
||||||
|
}, { immediate: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
60
src/composables/useLanguage.ts
Normal file
60
src/composables/useLanguage.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -299,5 +299,22 @@
|
|||||||
"title": "Verify your email",
|
"title": "Verify your email",
|
||||||
"description": "We have sent a verification code to {email}. Please enter the code below to verify your email address."
|
"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 / 语言"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,5 +299,22 @@
|
|||||||
"title": "验证您的邮箱",
|
"title": "验证您的邮箱",
|
||||||
"description": "我们已向 {email} 发送了一个验证码。请在下方输入代码以验证您的邮箱地址。"
|
"description": "我们已向 {email} 发送了一个验证码。请在下方输入代码以验证您的邮箱地址。"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "设置",
|
||||||
|
"language": "语言",
|
||||||
|
"aboutUs": "关于我们",
|
||||||
|
"clearCache": "清除缓存",
|
||||||
|
"checkUpdate": "检查更新",
|
||||||
|
"cacheCleared": "缓存已清除",
|
||||||
|
"updateAvailable": "发现新版本",
|
||||||
|
"currentVersion": "当前版本",
|
||||||
|
"latestVersion": "最新版本",
|
||||||
|
"newVersion": "新版本",
|
||||||
|
"cancel": "取消",
|
||||||
|
"updateNow": "立即更新",
|
||||||
|
"alreadyLatest": "已是最新版本",
|
||||||
|
"checkUpdateFailed": "检查更新失败",
|
||||||
|
"languageTitle": "语言 / Language"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { WritableComputedRef } from "vue";
|
||||||
|
import type { Locale } from "vue-i18n";
|
||||||
import { IonicVue } from "@ionic/vue";
|
import { IonicVue } from "@ionic/vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
import { createApp } from "vue";
|
import { createApp } from "vue";
|
||||||
|
|||||||
@@ -55,7 +55,17 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/system-settings",
|
path: "/system-settings",
|
||||||
component: () => import("@/views/system-settings/index.vue"),
|
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",
|
path: "/issue/issuing-apply",
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
<script lang="ts" setup>
|
<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";
|
import { checkbox, close, information, languageOutline, refresh } from "ionicons/icons";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const router = useIonRouter();
|
||||||
const { cacheSize, calculateCacheSize, clearCache } = useCacheSize();
|
const { cacheSize, calculateCacheSize, clearCache } = useCacheSize();
|
||||||
const { isChecking, checkForUpdate } = useAppUpdate();
|
const { isChecking, checkForUpdate } = useAppUpdate();
|
||||||
|
const { currentLanguage } = useLanguage();
|
||||||
|
|
||||||
function handleClearCache() {
|
function handleClearCache() {
|
||||||
clearCache();
|
clearCache();
|
||||||
calculateCacheSize();
|
calculateCacheSize();
|
||||||
toastController.create({
|
toastController.create({
|
||||||
message: "缓存已清除",
|
message: t("settings.cacheCleared"),
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
icon: checkbox,
|
icon: checkbox,
|
||||||
position: "bottom",
|
position: "bottom",
|
||||||
@@ -23,15 +26,15 @@ async function handleCheckUpdate() {
|
|||||||
|
|
||||||
if (result.hasUpdate) {
|
if (result.hasUpdate) {
|
||||||
const alert = await alertController.create({
|
const alert = await alertController.create({
|
||||||
header: "发现新版本",
|
header: t("settings.updateAvailable"),
|
||||||
message: `当前版本: ${result.currentVersion}\n最新版本: ${result.latestVersion || "新版本"}`,
|
message: `${t("settings.currentVersion")}: ${result.currentVersion}\n${t("settings.latestVersion")}: ${result.latestVersion || t("settings.newVersion")}`,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: "取消",
|
text: t("settings.cancel"),
|
||||||
role: "cancel",
|
role: "cancel",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "立即更新",
|
text: t("settings.updateNow"),
|
||||||
handler: () => {
|
handler: () => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
},
|
},
|
||||||
@@ -42,7 +45,7 @@ async function handleCheckUpdate() {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const toast = await toastController.create({
|
const toast = await toastController.create({
|
||||||
message: "已是最新版本",
|
message: t("settings.alreadyLatest"),
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
icon: checkbox,
|
icon: checkbox,
|
||||||
position: "bottom",
|
position: "bottom",
|
||||||
@@ -53,7 +56,7 @@ async function handleCheckUpdate() {
|
|||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
const toast = await toastController.create({
|
const toast = await toastController.create({
|
||||||
message: "检查更新失败",
|
message: t("settings.checkUpdateFailed"),
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
position: "bottom",
|
position: "bottom",
|
||||||
color: "danger",
|
color: "danger",
|
||||||
@@ -78,20 +81,20 @@ onMounted(() => {
|
|||||||
<ion-content :fullscreen="true" class="ion-padding">
|
<ion-content :fullscreen="true" class="ion-padding">
|
||||||
<ion-list lines="full">
|
<ion-list lines="full">
|
||||||
<ion-list-header>
|
<ion-list-header>
|
||||||
<ion-label>设置</ion-label>
|
<ion-label>{{ t("settings.title") }}</ion-label>
|
||||||
</ion-list-header>
|
</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 justify-between w-full items-center">
|
||||||
<div class="flex-center space-x-2">
|
<div class="flex-center space-x-2">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<ion-icon :icon="languageOutline" class="text-lg" />
|
<ion-icon :icon="languageOutline" class="text-lg" />
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm font-semibold">
|
<div class="text-sm font-semibold">
|
||||||
语言
|
{{ t("settings.language") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="end">
|
<div class="end">
|
||||||
简体中文
|
{{ currentLanguage.nativeName }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
@@ -102,7 +105,7 @@ onMounted(() => {
|
|||||||
<ion-icon :icon="information" class="text-lg" />
|
<ion-icon :icon="information" class="text-lg" />
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm font-semibold">
|
<div class="text-sm font-semibold">
|
||||||
关于我们
|
{{ t("settings.aboutUs") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,7 +117,7 @@ onMounted(() => {
|
|||||||
<ion-icon :icon="close" class="text-lg" />
|
<ion-icon :icon="close" class="text-lg" />
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm font-semibold">
|
<div class="text-sm font-semibold">
|
||||||
清除缓存
|
{{ t("settings.clearCache") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="end">
|
<div class="end">
|
||||||
@@ -129,7 +132,7 @@ onMounted(() => {
|
|||||||
<ion-icon :icon="refresh" class="text-lg" :class="{ 'animate-spin': isChecking }" />
|
<ion-icon :icon="refresh" class="text-lg" :class="{ 'animate-spin': isChecking }" />
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm font-semibold">
|
<div class="text-sm font-semibold">
|
||||||
检查更新
|
{{ t("settings.checkUpdate") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
58
src/views/system-settings/language.vue
Normal file
58
src/views/system-settings/language.vue
Normal 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%;
|
||||||
|
}
|
||||||
17
src/views/system-settings/outlet.vue
Normal file
17
src/views/system-settings/outlet.vue
Normal 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>
|
||||||
Reference in New Issue
Block a user