From 838e61ee448bd21588eb3474f2165d4f1587b0f4 Mon Sep 17 00:00:00 2001 From: 13895237362 <2205451508@qq.com> Date: Wed, 13 Aug 2025 10:15:37 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E5=A4=9A=E8=AF=AD?= =?UTF-8?q?=E8=A8=80=E9=85=8D=E7=BD=AE,=E6=8B=A6=E6=88=AA=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E9=85=8D=E7=BD=AE,=E9=94=99=E8=AF=AF=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E5=B7=B2=E5=90=8E=E7=AB=AF=E4=B8=BA=E4=B8=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.env.development | 5 +- frontend/.env.production | 5 +- frontend/.env.test | 3 - frontend/src/api/https/errorCodeType.ts | 8 ++ frontend/src/api/https/index.ts | 57 +++++---- frontend/src/i18n/index.ts | 64 +++++++--- frontend/src/store/modules/app.ts | 6 +- frontend/src/utils/language.ts | 150 ++++++++++++++++++++++++ 8 files changed, 248 insertions(+), 50 deletions(-) create mode 100644 frontend/src/utils/language.ts diff --git a/frontend/.env.development b/frontend/.env.development index 61476a8..4488a9e 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -6,11 +6,8 @@ # @FilePath: /frontend/.env.development # @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE ### -# 开发环境 -NODE_ENV = "development" - # 开发环境接口地址 -VITE_API_BASE_URL = '' +VITE_API_BASE_URL = 'http://104.156.140.42:18888' # # 开发环境打包名称 # VITE_OUTDIR='dev' diff --git a/frontend/.env.production b/frontend/.env.production index b52150f..75acfd8 100644 --- a/frontend/.env.production +++ b/frontend/.env.production @@ -1,8 +1,5 @@ -# 正式环境 -NODE_ENV = "production" - # 正式环境接口地址 -VITE_API_BASE_URL = '' +VITE_API_BASE_URL = 'http://104.156.140.42:18888' # 正式环境打包名称 VITE_OUTDIR='production' diff --git a/frontend/.env.test b/frontend/.env.test index 7c38831..a226fc4 100644 --- a/frontend/.env.test +++ b/frontend/.env.test @@ -6,9 +6,6 @@ # @FilePath: /frontend/.env.test # @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE ### -# 测试环境 -NODE_ENV = "test" - # 测试环境接口地址 VITE_API_BASE_URL = '' diff --git a/frontend/src/api/https/errorCodeType.ts b/frontend/src/api/https/errorCodeType.ts index e0de625..d72e0b2 100644 --- a/frontend/src/api/https/errorCodeType.ts +++ b/frontend/src/api/https/errorCodeType.ts @@ -1,3 +1,11 @@ +/* + * @Author: 13895237362 2205451508@qq.com + * @Date: 2025-07-31 14:30:20 + * @LastEditors: 13895237362 2205451508@qq.com + * @LastEditTime: 2025-08-13 09:38:31 + * @FilePath: /frontend/src/api/https/errorCodeType.ts + * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE + */ export const errorCodeType = function(status:Number):string{ let errMessage:string = "未知错误" switch (status) { diff --git a/frontend/src/api/https/index.ts b/frontend/src/api/https/index.ts index dca0d5e..805a4e6 100644 --- a/frontend/src/api/https/index.ts +++ b/frontend/src/api/https/index.ts @@ -2,8 +2,9 @@ import axios from 'axios' // http状态码 import { errorCodeType } from './errorCodeType' -//element-plus提示消息组件 -import { ElMessage } from "element-plus" +// 语言管理器 +import { generateAcceptLanguageHeader } from '@/utils/language' + const envUrl = import.meta.env.VITE_API_BASE_URL //使用create方法创建axios实例 @@ -13,8 +14,8 @@ const Axios = axios.create({ headers: { 'Content-Type': 'application/json;charset=UTF-8', }, - // 跨域时候允许携带凭证 - withCredentials: true + // 跨域时候允许携带凭证 (开发环境不启用,避免CORS问题) + withCredentials: false }) // 添加请求拦截器 Axios.interceptors.request.use(config => { @@ -23,6 +24,11 @@ Axios.interceptors.request.use(config => { if(token){ config.headers['Authorization'] = `Bearer ${token}` } + + // 设置 Accept-Language 头,符合 RFC 7231 规范 + // 优先级:用户选择;q=1.0 > 备用语言;q=0.8 > 通配符;q=0.5 + config.headers['Accept-Language'] = generateAcceptLanguageHeader() + return config }) // 添加响应拦截器 @@ -33,7 +39,7 @@ Axios.interceptors.response.use(response => { const code = apiData.code // 如果没有 Code, 代表这不是项目后端开发的 API if (code === undefined) { - ElMessage.error("非本系统的接口") + // 不在拦截器中显示错误,让组件自己处理 return Promise.reject(new Error("非本系统的接口")) } else { switch (code) { @@ -44,30 +50,35 @@ Axios.interceptors.response.use(response => { // code === 0 也代表成功 (兼容) return apiData default: - // 不是正确的 Code - ElMessage.error(apiData.message || "Error") - return Promise.reject(new Error("Error")) + // 不在拦截器中显示错误,让组件自己处理 + // 将错误信息附加到 Error 对象上,供组件使用 + const error = new Error(apiData.message || "Error") + ;(error as any).response = { data: apiData } + return Promise.reject(error) } } }, error => { console.log('Response的error', error) - showErrMessage(errorCodeType(error.response.status)) + // 不在拦截器中显示错误消息,让具体的页面组件处理 + // showErrMessage(errorCodeType(error.response?.status)) + + // 为错误对象添加更多信息,便于组件处理 + if (error.response) { + const status = error.response.status + const data = error.response.data + + // 如果后端返回了错误消息,优先使用后端消息 + if (data && data.message) { + ;(error as any).backendMessage = data.message + } else { + // 否则使用状态码对应的消息 + ;(error as any).backendMessage = errorCodeType(status) + } + } + return Promise.reject(error) }) -/** - * @description 显示错误消息 - * opt 传入参数 - * err 错误信息 - * type 消息类型 - * duration 消息持续时间 - */ -function showErrMessage (errMessage: string, type:any= 'error', duration:number = 5000){ - ElMessage({ - message: errMessage, - type:type, - duration: duration - }) -} +// 错误处理函数已移除,由页面组件统一处理错误显示 export default Axios; \ No newline at end of file diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts index e219e97..e485306 100644 --- a/frontend/src/i18n/index.ts +++ b/frontend/src/i18n/index.ts @@ -33,27 +33,39 @@ export const elementLocales = { // 支持的语言类型 export type SupportedLocale = 'en' | 'zh' | 'ja' +// 语言代码标准映射 +export const LANGUAGE_MAP = { + 'zh': { code: 'zh', iso: 'zh-CN', name: '中文' }, + 'en': { code: 'en', iso: 'en-US', name: 'English' }, + 'ja': { code: 'ja', iso: 'ja-JP', name: '日本語' } +} as const + // 检测浏览器语言并返回支持的语言 function detectBrowserLanguage(): SupportedLocale { - // 优先使用本地存储中的语言偏好 + // 1. 优先使用本地存储中的语言偏好(用户显式选择) const savedLanguage = localStorage.getItem('userLanguage') as SupportedLocale - if (savedLanguage && ['en', 'zh', 'ja'].includes(savedLanguage)) { + if (savedLanguage && Object.keys(LANGUAGE_MAP).includes(savedLanguage)) { return savedLanguage } - // 获取浏览器语言 - const browserLanguage = navigator.language.toLowerCase() + // 2. 检测浏览器语言设置 + const browserLanguages = navigator.languages || [navigator.language] - // 匹配浏览器语言到支持的语言 - if (browserLanguage.startsWith('en')) { - return 'en' - } else if (browserLanguage.startsWith('ja')) { - return 'ja' - } else if (browserLanguage.startsWith('zh')) { - return 'zh' + for (const lang of browserLanguages) { + const langCode = lang.toLowerCase() + + // 精确匹配 + if (langCode === 'zh-cn' || langCode === 'zh') return 'zh' + if (langCode === 'en-us' || langCode === 'en') return 'en' + if (langCode === 'ja-jp' || langCode === 'ja') return 'ja' + + // 前缀匹配 + if (langCode.startsWith('zh')) return 'zh' + if (langCode.startsWith('en')) return 'en' + if (langCode.startsWith('ja')) return 'ja' } - // 默认回退到中文 + // 3. 默认回退到中文 return 'zh' } @@ -67,12 +79,36 @@ const i18n = createI18n({ // 设置语言的函数 export function setI18nLanguage(locale: SupportedLocale) { + // 验证语言代码 + if (!Object.keys(LANGUAGE_MAP).includes(locale)) { + console.warn(`Unsupported locale: ${locale}, falling back to zh`) + locale = 'zh' + } + + // 更新 i18n 当前语言 i18n.global.locale.value = locale - // 设置 HTML 文档的 lang 属性 - document.documentElement.setAttribute('lang', locale) + + // 设置 HTML 文档的 lang 属性为标准 ISO 代码 + const isoCode = LANGUAGE_MAP[locale].iso + document.documentElement.setAttribute('lang', isoCode) + + // 保存用户语言偏好(用于下次访问和 Accept-Language 头) + localStorage.setItem('userLanguage', locale) + + // 触发自定义事件,通知其他组件语言已变更 + window.dispatchEvent(new CustomEvent('language-change', { + detail: { locale, isoCode, name: LANGUAGE_MAP[locale].name } + })) + return locale } +// 获取当前语言的 ISO 代码 +export function getCurrentLanguageISO(): string { + const currentLocale = i18n.global.locale.value as SupportedLocale + return LANGUAGE_MAP[currentLocale]?.iso || 'zh-CN' +} + // 获取当前Element Plus语言包 export function getCurrentElementLocale() { const currentLocale = i18n.global.locale.value as SupportedLocale diff --git a/frontend/src/store/modules/app.ts b/frontend/src/store/modules/app.ts index 62b06ea..2c57f40 100644 --- a/frontend/src/store/modules/app.ts +++ b/frontend/src/store/modules/app.ts @@ -6,7 +6,7 @@ export const useAppStore = defineStore('app', () => { const sidebarCollapsed = ref(false) const isMobile = ref(false) const theme = ref<'light' | 'dark' | 'auto'>('light') - const language = ref<'zh' | 'en'>('zh') + const language = ref<'zh' | 'en' | 'ja'>('zh') const loading = ref(false) // 计算属性 @@ -35,8 +35,10 @@ export const useAppStore = defineStore('app', () => { document.documentElement.setAttribute('data-theme', newTheme) } - const setLanguage = (lang: 'zh' | 'en') => { + const setLanguage = (lang: 'zh' | 'en' | 'ja') => { language.value = lang + // 同时更新 localStorage 以保持一致性 + localStorage.setItem('userLanguage', lang) } const setLoading = (isLoading: boolean) => { diff --git a/frontend/src/utils/language.ts b/frontend/src/utils/language.ts new file mode 100644 index 0000000..f1571ce --- /dev/null +++ b/frontend/src/utils/language.ts @@ -0,0 +1,150 @@ +/** + * 语言管理工具类 + * 提供统一的语言切换、检测和 Accept-Language 头管理 + */ + +import { setI18nLanguage, LANGUAGE_MAP, type SupportedLocale } from '@/i18n' +import { useAppStore } from '@/store/modules/app' + +// 导出类型供其他模块使用 +export type { SupportedLocale } + +export class LanguageManager { + private static instance: LanguageManager + + public static getInstance(): LanguageManager { + if (!LanguageManager.instance) { + LanguageManager.instance = new LanguageManager() + } + return LanguageManager.instance + } + + /** + * 切换语言并同步所有相关状态 + * @param locale 目标语言代码 + */ + public changeLanguage(locale: SupportedLocale): void { + try { + // 1. 更新 i18n + setI18nLanguage(locale) + + // 2. 更新 app store + const appStore = useAppStore() + appStore.setLanguage(locale) + + // 3. 记录日志 + console.log(`Language changed to: ${locale} (${LANGUAGE_MAP[locale].iso})`) + + } catch (error) { + console.error('Failed to change language:', error) + throw error + } + } + + /** + * 获取当前语言信息 + */ + public getCurrentLanguage(): { + code: SupportedLocale + iso: string + name: string + } { + const code = (localStorage.getItem('userLanguage') as SupportedLocale) || 'zh' + const languageInfo = LANGUAGE_MAP[code] + + return { + code, + iso: languageInfo.iso, + name: languageInfo.name + } + } + + /** + * 生成标准的 Accept-Language 头 + * 格式: 主语言;q=1.0, 备用语言;q=0.8, *;q=0.5 + */ + public generateAcceptLanguageHeader(): string { + const currentLang = this.getCurrentLanguage() + const primaryLang = currentLang.iso + const fallbackLang = primaryLang !== 'en-US' ? 'en-US' : 'zh-CN' + + return `${primaryLang};q=1.0, ${fallbackLang};q=0.8, *;q=0.5` + } + + /** + * 获取所有支持的语言列表 + */ + public getSupportedLanguages(): Array<{ + code: SupportedLocale + iso: string + name: string + }> { + return Object.entries(LANGUAGE_MAP).map(([code, info]) => ({ + code: code as SupportedLocale, + iso: info.iso, + name: info.name + })) + } + + /** + * 检测浏览器首选语言 + */ + public detectBrowserLanguage(): SupportedLocale { + const browserLanguages = navigator.languages || [navigator.language] + + for (const lang of browserLanguages) { + const langCode = lang.toLowerCase() + + // 精确匹配 + if (langCode === 'zh-cn' || langCode === 'zh') return 'zh' + if (langCode === 'en-us' || langCode === 'en') return 'en' + if (langCode === 'ja-jp' || langCode === 'ja') return 'ja' + + // 前缀匹配 + if (langCode.startsWith('zh')) return 'zh' + if (langCode.startsWith('en')) return 'en' + if (langCode.startsWith('ja')) return 'ja' + } + + return 'zh' // 默认中文 + } + + /** + * 初始化语言设置 + * 优先级:localStorage > 浏览器语言 > 默认中文 + */ + public initializeLanguage(): SupportedLocale { + const savedLanguage = localStorage.getItem('userLanguage') as SupportedLocale + + if (savedLanguage && Object.keys(LANGUAGE_MAP).includes(savedLanguage)) { + this.changeLanguage(savedLanguage) + return savedLanguage + } + + const browserLanguage = this.detectBrowserLanguage() + this.changeLanguage(browserLanguage) + return browserLanguage + } + + /** + * 监听语言变更事件 + */ + public onLanguageChange(callback: (event: CustomEvent) => void): void { + window.addEventListener('language-change', callback as EventListener) + } + + /** + * 移除语言变更监听器 + */ + public offLanguageChange(callback: (event: CustomEvent) => void): void { + window.removeEventListener('language-change', callback as EventListener) + } +} + +// 导出单例实例 +export const languageManager = LanguageManager.getInstance() + +// 导出便捷函数 +export const changeLanguage = (locale: SupportedLocale) => languageManager.changeLanguage(locale) +export const getCurrentLanguage = () => languageManager.getCurrentLanguage() +export const generateAcceptLanguageHeader = () => languageManager.generateAcceptLanguageHeader() \ No newline at end of file