🐛 fix i18n init bug

This commit is contained in:
landaiqing
2024-08-13 15:40:30 +08:00
parent 431e690f02
commit fa1301689a
24 changed files with 135 additions and 106 deletions

View File

@@ -13,7 +13,7 @@ VITE_API_BASE_URL='http://127.0.0.1:8080'
VITE_TITLE_NAME='五味子云相册' VITE_TITLE_NAME='五味子云相册'
# token key # token key
VITE_APP_TOKEN_KEY='schisandra' VITE_APP_TOKEN_KEY='Bearer'
# the websocket url # the websocket url
VITE_WEB_SOCKET_URL='ws://127.0.0.1:3010/wx/socket' VITE_WEB_SOCKET_URL='ws://127.0.0.1:3010/wx/socket'

View File

@@ -12,7 +12,7 @@ VITE_API_BASE_URL='http://1.95.0.111:5050'
VITE_TITLE_NAME='五味子云相册' VITE_TITLE_NAME='五味子云相册'
# token key # token key
VITE_APP_TOKEN_KEY='schisandra' VITE_APP_TOKEN_KEY='Bearer'
# the websocket url # the websocket url
VITE_WEB_SOCKET_URL='ws://127.0.0.1:3010/wx/socket' VITE_WEB_SOCKET_URL='ws://127.0.0.1:3010/wx/socket'

View File

@@ -14,7 +14,9 @@ export default [
{ {
rules: { rules: {
semi: "error", semi: "error",
"@typescript-eslint/no-explicit-any": "off" "@typescript-eslint/no-explicit-any": "off",
// "no-unused-vars": "off",
// "@typescript-eslint/no-unused-vars": ["off"],
} }
} }
]; ];

View File

@@ -4,8 +4,19 @@ import {service} from "@/utils/alova/service.ts";
export const getUserInfo = () => { export const getUserInfo = () => {
return service.Get('/api/auth/user/List', { return service.Get('/api/auth/user/List', {
meta: { meta: {
ignoreToken: true ignoreToken: false
}, },
}); });
}; };
export const refreshToken = (refreshToken: string) => {
return service.Get('/api/auth/token/refresh', {
params: {
refresh_token: refreshToken
},
meta: {
authRole: 'refreshToken'
}
});
};

View File

@@ -33,6 +33,9 @@ body {
justify-content: center; justify-content: center;
width: 600px; width: 600px;
height: 420px; height: 420px;
@media screen and (max-width: 768px) {
display: none;
}
} }
* { * {

View File

@@ -1,6 +1,6 @@
<template> <template>
<AConfigProvider <AConfigProvider
:locale="lang.getLang() === 'en' ? enUS : zhCN" :locale="lang.lang === 'en' ? enUS : zhCN"
:theme="app.themeConfig" :theme="app.themeConfig"
> >
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">

View File

@@ -2,7 +2,7 @@
import {createI18n} from 'vue-i18n'; import {createI18n} from 'vue-i18n';
import zh from './language/zh.ts'; import zh from './language/zh.ts';
import en from './language/en.ts'; import en from './language/en.ts';
import {parse} from "zipson/lib";
const messages = { const messages = {
en, en,
@@ -11,16 +11,25 @@ const messages = {
const language: string = (navigator.language || 'en').toLocaleLowerCase(); // 获取浏览器的语言 const language: string = (navigator.language || 'en').toLocaleLowerCase(); // 获取浏览器的语言
const lang: string = localStorage.getItem("lang") as string; function getLanguage(): string | null {
let lang: string | null = null;
const langStr: string | null = localStorage.getItem('lang');
if (langStr) {
lang = JSON.parse(langStr).lang;
}
return lang;
}
const i18n: any = createI18n({ const i18n: any = createI18n({
legacy: false, legacy: false,
compositionOnly: false, compositionOnly: false,
globalInjection: true, globalInjection: true,
silentTranslationWarn: true, silentTranslationWarn: true,
locale: parse(lang).lang || language.split('-')[0] || 'zh', // 首先从缓存里拿,没有的话就用浏览器语言, locale: getLanguage() || language.split('-')[0] || 'zh', // 首先从缓存里拿,没有的话就用浏览器语言,
silentFallbackWarn: true, silentFallbackWarn: true,
missingWarn: true, missingWarn: true,
fallbackWarn: false, fallbackWarn: false,
messages messages
}); })
;
export default i18n; export default i18n;

View File

@@ -34,6 +34,7 @@ export default {
rotateCaptchaTitle: "Please drag the slider to complete the puzzle", rotateCaptchaTitle: "Please drag the slider to complete the puzzle",
systemError: "System error, please try again later", systemError: "System error, please try again later",
captchaExpired: "captcha expired, please try again", captchaExpired: "captcha expired, please try again",
}, },
error: { error: {
networkError: 'Network error, please try again later', networkError: 'Network error, please try again later',
@@ -49,5 +50,7 @@ export default {
504: 'gateway timeout (504)', 504: 'gateway timeout (504)',
505: 'http version not supported (505)', 505: 'http version not supported (505)',
other: 'connect error', other: 'connect error',
authTokenError: "auth token error, please try again later",
authTokenExpired: "auth token expired, please login again",
} }
}; };

View File

@@ -35,6 +35,7 @@ export default {
systemError: "系统错误!请稍后再试!", systemError: "系统错误!请稍后再试!",
captchaExpired: "验证码已过期,请重新获取!", captchaExpired: "验证码已过期,请重新获取!",
}, },
error: { error: {
networkError: '网络连接失败!', networkError: '网络连接失败!',
@@ -50,6 +51,8 @@ export default {
504: '网关超时(504)', 504: '网关超时(504)',
505: 'HTTP版本不受支持(505)', 505: 'HTTP版本不受支持(505)',
other: '连接出错', other: '连接出错',
authTokenError: "认证失败,请重新登录!",
authTokenExpired: "认证过期,请重新登录!",
} }
}; };

View File

@@ -3,14 +3,18 @@ import App from './App.vue';
import '@/assets/styles/scroll-bar.scss'; import '@/assets/styles/scroll-bar.scss';
import '@/assets/styles/global.scss'; import '@/assets/styles/global.scss';
import i18n from "@/locales/index.ts"; import i18n from "@/locales/index.ts";
import {setupStore} from "@/store/pinia.ts";
import router from "@/router/router.ts"; import router from "@/router/router.ts";
import "go-captcha-vue/dist/style.css" import "go-captcha-vue/dist/style.css";
import GoCaptcha from "go-captcha-vue" import GoCaptcha from "go-captcha-vue";
import {createPinia, Pinia} from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
const pinia: Pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
const app = createApp(App); const app = createApp(App);
setupStore(app); app.use(pinia);
app.use(router); app.use(router);
app.use(i18n); app.use(i18n);
app.use(GoCaptcha) app.use(GoCaptcha);
app.mount('#app'); app.mount('#app');

View File

@@ -1,25 +1,12 @@
import {defineStore} from 'pinia'; import {defineStore} from 'pinia';
import {ref} from 'vue'; import {ref} from "vue";
import pinia from "@/store/pinia.ts";
import {parse, stringify} from "zipson/lib";
export const langStore = defineStore( export const langStore = defineStore(
'lang', 'lang',
() => { () => {
const lang = ref<string>('zh'); const lang = ref<string>('zh');
function setLang(value: string) {
lang.value = value;
}
function getLang() {
return lang.value;
}
return { return {
lang, lang,
setLang,
getLang,
}; };
}, },
{ {
@@ -28,14 +15,6 @@ export const langStore = defineStore(
key: 'lang', key: 'lang',
storage: localStorage, storage: localStorage,
paths: ["lang"], paths: ["lang"],
serializer: {
deserialize: parse,
serialize: stringify,
},
} }
} }
); );
export function useLangStoreWithOut() {
return langStore(pinia);
}

View File

@@ -2,7 +2,6 @@ import {defineStore} from 'pinia';
import {computed, ref} from 'vue'; import {computed, ref} from 'vue';
import {theme} from 'ant-design-vue'; import {theme} from 'ant-design-vue';
import variables from '@/assets/styles/colors.module.scss'; import variables from '@/assets/styles/colors.module.scss';
import {parse, stringify} from "zipson/lib";
/** /**
* theme 配置 * theme 配置
@@ -45,10 +44,6 @@ export const useThemeStore = defineStore(
key: 'theme', key: 'theme',
storage: localStorage, storage: localStorage,
paths: ["themeName", "darkMode"], paths: ["themeName", "darkMode"],
serializer: {
deserialize: parse,
serialize: stringify,
},
} }
} }
); );

View File

@@ -1,7 +1,6 @@
import {defineStore} from 'pinia'; import {defineStore} from 'pinia';
import {ref} from 'vue'; import {ref} from 'vue';
import {User} from "@/types/user"; import {User} from "@/types/user";
import {parse, stringify} from "zipson/lib";
export const useAuthStore = defineStore( export const useAuthStore = defineStore(
@@ -34,10 +33,6 @@ export const useAuthStore = defineStore(
key: 'user', key: 'user',
storage: localStorage, storage: localStorage,
paths: ["user"], paths: ["user"],
serializer: {
deserialize: parse,
serialize: stringify,
},
} }
} }
); );

View File

@@ -1,12 +0,0 @@
import {createPinia, Pinia} from "pinia";
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import {App} from "vue";
const pinia: Pinia = createPinia();
pinia.use(piniaPluginPersistedstate)
export function setupStore(app: App<Element>) {
app.use(pinia);
}
export default pinia;

4
src/types/user.d.ts vendored
View File

@@ -1,6 +1,8 @@
export interface User { export interface User {
token?: string accessToken?: string
userId?: string userId?: string
refreshToken?: string
expiresAt?: number
} }

View File

@@ -6,7 +6,7 @@ export const localforageStorageAdapter = {
localforage.setItem(key, value).then(); localforage.setItem(key, value).then();
}, },
get(key: string) { get(key: string) {
let value; let value: any;
localforage.getItem(key).then((res: any) => { localforage.getItem(key).then((res: any) => {
if (res === null || res === undefined) { if (res === null || res === undefined) {
value = ""; value = "";

View File

@@ -2,13 +2,46 @@ import {createAlova} from 'alova';
import VueHook from 'alova/vue'; import VueHook from 'alova/vue';
import useStore from "@/store"; import useStore from "@/store";
import {axiosRequestAdapter} from "@alova/adapter-axios";
import {AxiosError, AxiosResponse} from "axios";
import {message} from "ant-design-vue";
import {localforageStorageAdapter} from "@/utils/alova/adapter/localforageStorageAdapter.ts"; import {localforageStorageAdapter} from "@/utils/alova/adapter/localforageStorageAdapter.ts";
import {createServerTokenAuthentication} from "alova/client";
import {AxiosError, AxiosResponse} from "axios";
import {handleCode} from "@/utils/errorCode/errorCodeHandler.ts"; import {handleCode} from "@/utils/errorCode/errorCodeHandler.ts";
import {message} from "ant-design-vue";
import i18n from "@/locales"; import i18n from "@/locales";
import {axiosRequestAdapter} from "@alova/adapter-axios";
import {refreshToken} from "@/api/user";
import router from "@/router/router.ts";
const {onAuthRequired, onResponseRefreshToken} = createServerTokenAuthentication<typeof VueHook,
typeof axiosRequestAdapter>({
refreshTokenOnSuccess: {
// 在请求前触发将接收到method参数并返回boolean表示token是否过期
isExpired: (response: AxiosResponse<any, any>, _method: any) => {
return response.data.code === 401;
},
// 当token过期时触发在此函数中触发刷新token
handler: async () => {
try {
const user = useStore().user;
const res: any = await refreshToken(user.getUser()?.refreshToken || '');
if (res.code === 0 && res.data) {
user.setUser({
userId: res.data.userId,
accessToken: res.data.access_token,
refreshToken: res.data.refresh_token,
expiresAt: res.data.expires_at,
});
}
} catch (error) {
// token刷新失败跳转回登录页
message.error(i18n.global.t('error.authTokenError')).then();
await router.push('/login');
throw error;
}
}
}
});
export const service = createAlova({ export const service = createAlova({
timeout: 5000, timeout: 5000,
baseURL: import.meta.env.VITE_APP_BASE_API, baseURL: import.meta.env.VITE_APP_BASE_API,
@@ -16,43 +49,38 @@ export const service = createAlova({
// 请求适配器 // 请求适配器
requestAdapter: axiosRequestAdapter(), requestAdapter: axiosRequestAdapter(),
l2Cache: localforageStorageAdapter, l2Cache: localforageStorageAdapter,
cacheFor: {
// GET: {
// mode: 'restore',
// expire: 10 * 1000
// },
},
cacheLogger: import.meta.env.VITE_NODE_ENV === 'development', cacheLogger: import.meta.env.VITE_NODE_ENV === 'development',
cacheFor: {},
// 设置全局的请求拦截器 // 设置全局的请求拦截器
beforeRequest(method) { beforeRequest: onAuthRequired(async (method: any) => {
if (!method.meta?.ignoreToken) { if (!method.meta?.ignoreToken) {
const user = useStore().user; const user = useStore().user;
method.config.headers.token = user.getUser()?.token; method.config.headers.Authorization = `${import.meta.env.VITE_APP_TOKEN_KEY} ${user.getUser()?.accessToken}`;
} }
const lang = useStore().lang; const lang = useStore().lang;
method.config.headers['Accept-Language'] = lang.getLang(); method.config.headers['Accept-Language'] = lang.lang|| 'zh';
}),
},
// 响应拦截器 // 响应拦截器
responded: { responded: onResponseRefreshToken({
onSuccess: async (response: AxiosResponse) => { onSuccess: async (response: AxiosResponse, _method: any) => {
if (response.data instanceof Blob) { if (response.data instanceof Blob) {
return response; return response;
} else { } else {
return response.data; return response.data;
} }
}, },
onError(error: AxiosError) { onError:
const {response} = error; (error: AxiosError, _method: any) => {
if (response) { const {response} = error;
handleCode(response.status); if (response) {
} handleCode(response.status);
if (!window.navigator.onLine) { }
message.error(i18n.global.t('error.networkError')).then(); if (!window.navigator.onLine) {
return Promise.reject(error); message.error(i18n.global.t('error.networkError')).then();
} return Promise.reject(error);
} }
} },
}),
}); });

View File

@@ -14,7 +14,7 @@ class Request {
this.instance.interceptors.request.use( this.instance.interceptors.request.use(
(config) => { (config) => {
const user = useStore().user; const user = useStore().user;
const token: string | undefined = user.getUser()?.token; const token: string | undefined = user.getUser()?.accessToken;
if (token) { if (token) {
config.headers.Authorization = `${import.meta.env.VITE_APP_TOKEN_KEY} ${token}`; config.headers.Authorization = `${import.meta.env.VITE_APP_TOKEN_KEY} ${token}`;
} }

View File

@@ -14,6 +14,7 @@
background-attachment: fixed; background-attachment: fixed;
/* 让背景图基于容器大小伸缩 */ /* 让背景图基于容器大小伸缩 */
background-size: cover; background-size: cover;
z-index: -1;
.forget-left { .forget-left {
width: 50%; width: 50%;

View File

@@ -260,8 +260,9 @@ async function sendCaptcha() {
phoneLoginFormRef.value phoneLoginFormRef.value
.validateFields("phone") .validateFields("phone")
.then(() => { .then(() => {
showRotateCaptcha.value = true; getRotateCaptcha().then(() => {
getRotateCaptcha(); showRotateCaptcha.value = true;
});
}) })
.catch((error: any) => { .catch((error: any) => {
console.log('error', error); console.log('error', error);
@@ -315,10 +316,11 @@ async function getRotateCaptcha() {
* @param angle * @param angle
*/ */
async function checkCaptcha(angle: number) { async function checkCaptcha(angle: number) {
if (captchaErrorCount.value >= 1) { if (captchaErrorCount.value >= 2) {
message.error(t('login.captchaError')); message.error(t('login.captchaError'));
getRotateCaptcha().then(); getRotateCaptcha().then(() => {
captchaErrorCount.value = 0; captchaErrorCount.value = 0;
});
} else { } else {
const result: any = await checkRotatedCaptcha(angle, captchaData.key); const result: any = await checkRotatedCaptcha(angle, captchaData.key);
if (result.code === 0 && result.success) { if (result.code === 0 && result.success) {
@@ -327,8 +329,10 @@ async function checkCaptcha(angle: number) {
countDown(); countDown();
} else if (result.code === 1011) { } else if (result.code === 1011) {
message.error(t('login.captchaExpired')); message.error(t('login.captchaExpired'));
getRotateCaptcha().then(); getRotateCaptcha().then(() => {
captchaErrorCount.value = 0; captchaErrorCount.value = 0;
});
} else { } else {
captchaErrorCount.value++; captchaErrorCount.value++;
message.error(t('login.captchaError')); message.error(t('login.captchaError'));

View File

@@ -14,6 +14,7 @@
background-attachment: fixed; background-attachment: fixed;
/* 让背景图基于容器大小伸缩 */ /* 让背景图基于容器大小伸缩 */
background-size: cover; background-size: cover;
z-index: -1;
.login-left { .login-left {
width: 50%; width: 50%;

View File

@@ -14,6 +14,7 @@
background-attachment: fixed; background-attachment: fixed;
/* 让背景图基于容器大小伸缩 */ /* 让背景图基于容器大小伸缩 */
background-size: cover; background-size: cover;
z-index: -1;
.qrlogin-left { .qrlogin-left {
width: 50%; width: 50%;

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="test"> <div class="test">
<AButton @click="handleClick"> {{ t('login.test') }}</AButton> <AButton @click="handleClick"> {{ t('login.title') }}</AButton>
<AButton @click="changeLang('zh')"> 切换中文</AButton> <AButton @click="changeLang('zh')"> 切换中文</AButton>
<AButton @click="changeLang('en')"> 切换英文</AButton> <AButton @click="changeLang('en')"> 切换英文</AButton>
@@ -27,9 +27,8 @@ const handleClick = () => {
const lang = useStore().lang; const lang = useStore().lang;
async function changeLang(language: any) { async function changeLang(language: any) {
lang.setLang(language); lang.lang = language;
locale.value = language; locale.value = language;
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -14,6 +14,7 @@ import {fileURLToPath} from 'node:url';
export default defineConfig(({mode}: { mode: string }): object => { export default defineConfig(({mode}: { mode: string }): object => {
const env: Record<string, string> = loadEnv(mode, process.cwd()); const env: Record<string, string> = loadEnv(mode, process.cwd());
return { return {
base: '/',
resolve: { resolve: {
//设置别名 //设置别名
alias: { alias: {