feat(i18n): 重构国际化系统并优化语言切换功能

- 采用 Pinia store 统一管理语言状态
- 实现简洁高效的语言切换逻辑
- 优化路由导航和页面刷新策略
- 移除冗余代码,提高系统可维护性
- 新增 useLanguageSwitch 和 useAppInit 组合式 API
This commit is contained in:
linear 2025-08-29 14:12:54 +08:00
parent 15cdaee012
commit ffb5e9cc54
25 changed files with 796 additions and 651 deletions

116
INTERNATIONALIZATION.md Normal file
View File

@ -0,0 +1,116 @@
# 国际化系统重构说明
## 概述
本项目已重构国际化系统,采用更简洁、稳定的架构,解决了原有的语言切换逻辑混乱问题。
## 新架构特点
### 1. 统一的状态管理
- 使用 Pinia store (`src/stores/language.js`) 统一管理语言状态
- 支持 localStorage 持久化用户语言选择
- 自动检测浏览器语言偏好
### 2. 简化的语言切换
- 使用 `useLanguageSwitch` composable 处理语言切换
- 自动处理路由前缀(中文页面添加 `/cn` 前缀)
- 无需强制页面刷新,提供流畅的用户体验
### 3. 清晰的职责分离
- **语言状态管理**: `useLanguageStore`
- **语言切换逻辑**: `useLanguageSwitch`
- **路由导航**: `useNavigation`
- **初始化**: `language-init.client.js` 插件
## 核心文件
### 语言状态管理
```javascript
// src/stores/language.js
export const useLanguageStore = defineStore('language', () => {
const currentLocale = ref('en')
const availableLocales = [
{ code: 'en', name: 'English' },
{ code: 'cn', name: '简体中文' }
]
// 设置语言、初始化、切换等方法
})
```
### 语言切换逻辑
```javascript
// src/composables/useLanguageSwitch.js
export const useLanguageSwitch = () => {
const switchLanguage = async (newLocale) => {
// 更新状态并处理路由跳转
}
const initLanguage = () => {
// 初始化语言状态
}
})
```
## 使用方法
### 在组件中切换语言
```vue
<script setup>
import { useLanguageSwitch } from '@/composables/useLanguageSwitch.js'
const { switchLanguage, currentLocale } = useLanguageSwitch()
const changeToChinese = () => {
switchLanguage('cn')
}
const changeToEnglish = () => {
switchLanguage('en')
}
</script>
```
### 在组件中使用翻译
```vue
<template>
<h1>{{ $t('page.home') }}</h1>
<p>{{ $t('nav.about') }}</p>
</template>
```
## 路由策略
- **默认语言**: 英文 (en)
- **策略**: `prefix_except_default`
- **中文页面**: 自动添加 `/cn` 前缀
- **英文页面**: 无前缀
### 路由示例
- 英文首页: `/`
- 中文首页: `/cn`
- 英文关于我们: `/about-us`
- 中文关于我们: `/cn/about-us`
## 语言检测逻辑
1. **优先使用**: 用户手动选择的语言localStorage
2. **其次使用**: 浏览器语言检测
3. **默认语言**: 英文
## 测试
访问 `/test-language` 页面可以测试语言切换功能。
## 移除的旧文件
- `src/composables/useLanguageDetection.js` - 被新的 `useLanguageSwitch` 替代
- `src/hook/lang.js` - 功能整合到 store 中
## 优势
1. **代码简洁**: 移除了重复和冲突的逻辑
2. **状态一致**: 统一的状态管理确保服务端和客户端一致
3. **用户体验**: 无需页面刷新,流畅的语言切换
4. **可维护性**: 清晰的职责分离,易于维护和扩展
5. **性能优化**: 减少了不必要的 Cookie 操作和页面刷新

View File

@ -49,28 +49,31 @@ export default defineNuxtConfig({
{ src: '~/plugins/aos-client.js', mode: 'client' }, { src: '~/plugins/aos-client.js', mode: 'client' },
'~/plugins/vue-dompurify-html.js', '~/plugins/vue-dompurify-html.js',
'~/plugins/image-path.js', '~/plugins/image-path.js',
{ src: '~/plugins/static-data.client.js', mode: 'client' } { src: '~/plugins/static-data.client.js', mode: 'client' },
{ src: '~/plugins/language-init.client.js', mode: 'client' }
], ],
devServer: { devServer: {
port: 1110, port: 1100,
}, },
modules: ['@nuxtjs/i18n', '@pinia/nuxt'], modules: ['@nuxtjs/i18n', '@pinia/nuxt'],
i18n: { i18n: {
locales: [ locales: [
{ code: 'en', name: 'English' }, { code: 'en', name: 'English', file: 'en.json' },
{ code: 'cn', name: '简体中文' } { code: 'cn', name: '简体中文', file: 'cn.json' }
], ],
defaultLocale: 'en', defaultLocale: 'en',
detectBrowserLanguage: { detectBrowserLanguage: {
useCookie: true, // 启用 Nuxt i18n 的自动检测
useCookie: true, // 使用 cookie 保存语言选择
cookieKey: 'i18n_redirected', cookieKey: 'i18n_redirected',
redirectOn: 'root', redirectOn: 'root', // 只在根路径时重定向
alwaysRedirect: true, alwaysRedirect: true,
fallbackLocale: 'en', fallbackLocale: 'en'
cookieSecure: false
}, },
strategy: 'prefix_except_default', strategy: 'prefix_except_default',
vueI18n: '~/i18n.config.ts' vueI18n: '~/i18n.config.ts',
langDir: 'locale', // 修正路径相对于src目录
debug: false // 关闭调试模式,减少控制台输出
}, },
// Axios配置 // Axios配置
runtimeConfig: { runtimeConfig: {

View File

@ -4,7 +4,8 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "nuxt build", "build": "nuxt build",
"dev": "nuxt dev", "dev": "nuxt dev --host 0.0.0.0 --port 1110",
"dev:custom": "node scripts/dev.js",
"generate": "nuxt generate", "generate": "nuxt generate",
"generate:static": "node scripts/generate-static.js && nuxi generate", "generate:static": "node scripts/generate-static.js && nuxi generate",
"preview": "nuxt preview", "preview": "nuxt preview",

32
scripts/dev.js Normal file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env node
const { spawn } = require('child_process');
const os = require('os');
// 获取本机 IP 地址
function getLocalIP() {
const interfaces = os.networkInterfaces();
for (const name of Object.keys(interfaces)) {
for (const interface of interfaces[name]) {
if (interface.family === 'IPv4' && !interface.internal) {
return interface.address;
}
}
}
return 'localhost';
}
const localIP = getLocalIP();
console.log(`🚀 启动开发服务器...`);
console.log(`📱 本机访问: http://localhost:1110`);
console.log(`🌐 内网访问: http://${localIP}:1110`);
// 启动 Nuxt 开发服务器
const child = spawn('npx', ['nuxt', 'dev', '--host', '0.0.0.0', '--port', '1110'], {
stdio: 'inherit',
shell: true
});
child.on('close', (code) => {
console.log(`开发服务器已停止,退出码: ${code}`);
});

View File

@ -1,30 +1,15 @@
export default defineEventHandler((event: any) => { export default defineEventHandler((event: any) => {
// 服务端中间件,直接执行 // 设置基本的国际化响应头
const cookie = getCookie(event, 'i18n_redirected'); setHeader(event, 'X-Detected-Language', 'en');
const acceptLanguage = getHeader(event, 'accept-language') || '';
// 添加 CORS 支持
let detectedLang = 'en'; // 默认英文 setHeader(event, 'Access-Control-Allow-Origin', '*');
setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
// 优先检查 cookie如果有 cookie 就使用 cookie 中的语言 setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (cookie && ['cn', 'en'].includes(cookie)) {
detectedLang = cookie; // 处理 OPTIONS 请求
} if (getMethod(event) === 'OPTIONS') {
// 如果没有 cookie则根据浏览器语言检测 setResponseStatus(event, 200);
else if (acceptLanguage.includes('zh')) { return '';
detectedLang = 'cn'; }
}
// 设置到 context 供后续使用
event.context.locale = detectedLang;
// 设置响应头
setHeader(event, 'X-Detected-Language', detectedLang);
// 如果是根路径且没有cookie根据浏览器语言进行重定向
if (event.path === '/' && !cookie) {
if (acceptLanguage.includes('zh')) {
// 重定向到中文版本
return sendRedirect(event, '/cn', 302);
}
}
}); });

View File

@ -1,218 +1,189 @@
<template> <template>
<div> <div>
<keep-alive v-if="isKeepAlive"> <keep-alive v-if="isKeepAlive">
<NuxtPage /> <NuxtPage />
</keep-alive> </keep-alive>
<NuxtPage v-else /> <NuxtPage v-else />
<back-top v-if="currentPath !== '/404'" /> <back-top v-if="currentPath !== '/404'" />
</div> </div>
</template> </template>
<script setup> <script setup>
import { import { computed, onMounted } from "vue";
computed import { useRoute } from "#imports";
} from 'vue'; import BackTop from "@/components/back-top/back-top.vue";
import { import { useAppInit } from "@/composables/useAppInit.js";
useRoute
} from '#imports'; //
import BackTop from "@/components/back-top/back-top.vue"; const route = useRoute();
//
// const currentPath = computed(() => route.fullPath);
const route = useRoute(); // meta
// const isKeepAlive = computed(() => {
const currentPath = computed(() => route.fullPath); return route.meta.keepAlive;
// meta });
const isKeepAlive = computed(() => {
return route.meta.keepAlive; //
}); const { initApp } = useAppInit();
useHead({ onMounted(() => {
title: '明阳良光', //
meta: [{ initApp();
name: 'viewport', });
content: 'width=device-width, initial-scale=1'
}, useHead({
{ title: "明阳良光",
hid: 'description', meta: [
name: 'description', {
content: '明阳良光' name: "viewport",
}, content: "width=device-width, initial-scale=1",
{ },
name: 'theme-color', {
content: '#4f8cef' hid: "description",
} name: "description",
], content: "明阳良光",
link: [ },
{ {
hid: 'favicon', name: "theme-color",
rel: 'icon', content: "#4f8cef",
href: '/favicon.ico' },
}, ],
{ link: [
hid: 'iconfont', {
rel: 'stylesheet', hid: "favicon",
href: '/static/font/iconfont.css' rel: "icon",
} href: "/favicon.ico",
], },
script: [{ {
innerHTML: ` hid: "iconfont",
rel: "stylesheet",
href: "/static/font/iconfont.css",
},
],
script: [
{
innerHTML: `
(function() { (function() {
// Vue //
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
try { try {
// 使cookieNuxt i18n //
let savedLang = 'en'; const currentPath = window.location.pathname;
const currentLang = currentPath.startsWith('/cn') ? 'cn' : 'en';
// 1. cookie //
const cookies = document.cookie.split(';'); document.documentElement.setAttribute('lang', currentLang === 'cn' ? 'zh-CN' : 'en');
const localeCookie = cookies.find(cookie => document.documentElement.setAttribute('data-locale', currentLang);
cookie.trim().startsWith('i18n_redirected=')
);
if (localeCookie) {
const langValue = localeCookie.split('=')[1];
if (['cn', 'en'].includes(langValue)) {
savedLang = langValue;
}
}
// 2. cookielocalStorage //
if (savedLang === 'en') { if (document.title) {
const storedLang = localStorage.getItem('locale_lang'); document.title = document.title + (currentLang === 'cn' ? ' - 明阳良光' : ' - Mingyang Liangguang');
if (storedLang && ['cn', 'en'].includes(storedLang)) {
savedLang = storedLang;
}
}
// 3.
if (savedLang === 'en') {
const browserLang = navigator.language.toLowerCase();
if (browserLang.startsWith("en")) {
savedLang = "en";
} else if (browserLang.includes("zh") || browserLang.includes("cn")) {
savedLang = "cn";
}
}
//
document.documentElement.setAttribute('data-initial-lang', savedLang);
document.documentElement.setAttribute('lang', savedLang === 'cn' ? 'zh-CN' : 'en');
document.documentElement.setAttribute('data-locale', savedLang);
window.__INITIAL_LANG__ = savedLang;
// i18n
window.__I18N_INITIAL_LOCALE__ = savedLang;
// cookielocalStorage
if (savedLang !== 'en') {
document.cookie = 'i18n_redirected=' + savedLang + '; path=/; max-age=31536000';
localStorage.setItem('locale_lang', savedLang);
} }
} catch (e) { } catch (e) {
const defaultLang = 'en'; console.warn('Language setup failed:', e);
document.documentElement.setAttribute('data-initial-lang', defaultLang); //
document.documentElement.setAttribute('lang', 'en'); document.documentElement.setAttribute('lang', 'en');
document.documentElement.setAttribute('data-locale', defaultLang); document.documentElement.setAttribute('data-locale', 'en');
window.__INITIAL_LANG__ = defaultLang;
window.__I18N_INITIAL_LOCALE__ = defaultLang;
} }
} }
})(); })();
`, `,
type: 'text/javascript' type: "text/javascript",
}] },
}) ],
});
</script> </script>
<style lang="scss"> <style lang="scss">
@import './common/css/common.scss'; @import "./common/css/common.scss";
// - // -
html:not([data-locale]) { html:not([data-locale]) {
// //
body { body {
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
} }
} }
// //
html[data-locale] { html[data-locale] {
body { body {
opacity: 1; opacity: 1;
} }
} }
// //
html { html {
transition: all 0.1s ease; transition: all 0.1s ease;
} }
// //
html[data-locale="en"] { html[data-locale="en"] {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
} }
html[data-locale="cn"] { html[data-locale="cn"] {
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif; font-family: -apple-system, BlinkMacSystemFont, "PingFang SC",
} "Microsoft YaHei", sans-serif;
}
// //
.navbar { .navbar {
opacity: 1; opacity: 1;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
} }
// //
* { * {
transition: none; transition: none;
} }
// //
.navbar, .navbar,
.menu-item, .menu-item,
.language-selector, .language-selector,
.mobile-menu { .mobile-menu {
transition: all 0.3s ease; transition: all 0.3s ease;
} }
// //
.language-selector { .language-selector {
min-width: 80px; // min-width: 80px; //
} }
.lang-text { .lang-text {
min-width: 40px; // min-width: 40px; //
text-align: center; text-align: center;
} }
@font-face { @font-face {
font-family: MiSans-Light; font-family: MiSans-Light;
src: url("/static/font/MiSans-Light.ttf"); src: url("/static/font/MiSans-Light.ttf");
} }
@font-face { @font-face {
font-family: MiSans-Bold; font-family: MiSans-Bold;
src: url("/static/font/MiSans-Bold.ttf"); src: url("/static/font/MiSans-Bold.ttf");
} }
@font-face { @font-face {
font-family: MiSans-Medium; font-family: MiSans-Medium;
src: url("/static/font/MiSans-Medium.ttf"); src: url("/static/font/MiSans-Medium.ttf");
} }
@font-face { @font-face {
font-family: MiSans-Regular; font-family: MiSans-Regular;
src: url("/static/font/MiSans-Regular.ttf"); src: url("/static/font/MiSans-Regular.ttf");
} }
@font-face { @font-face {
font-family: MiSans-Normal; font-family: MiSans-Normal;
src: url("/static/font/MiSans-Normal.ttf"); src: url("/static/font/MiSans-Normal.ttf");
} }
@font-face { @font-face {
font-family: MiSans-SemiBold; font-family: MiSans-SemiBold;
src: url("/static/font/MiSans-Semibold.ttf"); src: url("/static/font/MiSans-Semibold.ttf");
} }
</style> </style>

View File

@ -1,131 +1,16 @@
<script setup name="footer"> <script setup name="footer">
import { ref, reactive, onUnmounted, onMounted } from "vue"; import { ref } from "vue";
import { useI18n } from "#imports"; import { useI18n } from "#imports";
import { GetMessageApi } from "@/service/api.js";
import { useRoute } from "#imports";
import { useNavigation } from "@/composables/useNavigation.js"; import { useNavigation } from "@/composables/useNavigation.js";
const { t } = useI18n(); import { useLanguageStore } from "@/stores/language.js";
import { GetProductCategoryApi } from "~/service/api.js";
// const { t } = useI18n();
import useSheller from "~/stores/seller.js"; const languageStore = useLanguageStore();
// const langIs = ref(languageStore.currentLocale);
import langToCheck from "~/hook/lang.js"; const { navigateTo } = useNavigation();
const langIs = ref(langToCheck());
const route = useRoute();
const { navigateTo } = useNavigation();
//
const Store = useSheller();
const props = defineProps({
//
marginSpacing: {
type: Boolean,
default: false,
},
});
const subRouteEvent = (subParmas, item) => {
let secondaryRoute = subParmas.enName;
switch (secondaryRoute) {
case "Power Station": {
Store.clickTab = 0;
navigateTo("product", { query: { CategoryId: 0 } });
break;
}
case "Solar Panel": {
Store.clickTab = 1;
navigateTo("product", { query: { CategoryId: 1 } });
break;
}
case "Accessory": {
Store.clickTab = 2;
navigateTo("product", { query: { CategoryId: 2 } });
break;
}
case "About Us": {
navigateTo("about-us");
break;
}
case "Contact": {
navigateTo("contact-us");
break;
}
default: {
//
navigateTo("home");
break;
}
}
footTabs.forEach((it) => {
(it.checked = false), (it.expand = false);
});
};
// " - "
const FlowReplace = (str) => {
return str.toLowerCase().replace(/\s+/g, "-");
};
//expandwei
const footTabs = reactive([]);
const GetProductCategorylist = () => {
GetProductCategoryApi().then((res) => {
let arrofObjects = res.rows;
Store.categoryList = arrofObjects.map((obj, k) => {
return {
...obj,
categorId: k,
path: "product/product",
Params: k,
};
});
footTabs[0].dropList = Store.categoryList;
});
};
onMounted(() => {
footTabs.push(
{
enName: "Product",
name: "产品系列",
expand: false,
dropList: [
{
enName: "监控摄像头",
name: "监控摄像头",
},
{
enName: "LED灯泡",
name: "LED灯泡",
},
{
enName: "车灯",
name: "车灯",
},
],
},
{
enName: "About",
name: "关于我们",
expand: false,
dropList: [
{
enName: "About Us",
name: "关于我们",
path: "/about-us"
},
],
},
{
enName: "Contact",
name: "联系我们",
expand: false,
dropList: [
{
enName: "Contact",
name: "联系我们",
path: "/contact-us/contact-us"
},
],
}
);
});
// //
const GoAgreement = (value) => { const GoAgreement = (value) => {
switch (value) { switch (value) {
@ -163,17 +48,7 @@ const GoAgreement = (value) => {
} }
} }
}; };
//
const TransitTap = (symbolName) => {
footTabs.forEach((it) => {
symbolName == it.enName ? (it.expand = !it.expand) : (it.expand = false);
});
};
//
const goTab = (path) => {
navigateTo(path);
};
</script> </script>
<template> <template>
@ -234,13 +109,13 @@ const goTab = (path) => {
</div> </div>
</div> </div>
<!-- 末尾 --> <!-- 末尾 -->
<div class="foot-bottom" v-if="langIs === 'cn'"> <div class="foot-bottom">
<div class="foot-bottom__box"> <div class="foot-bottom__box">
<div class="foot-bottom__box__content"> <div class="foot-bottom__box__content">
<p class="foot-bottom__box__content__copyright"> <p class="foot-bottom__box__content__copyright">
版权所有 © 2025 明阳良光 | {{ t('footer.copyright') }} |
<a @click="GoAgreement(5)" class="foot-bottom__box__content__link">粤ICP备2023070569号</a> | <a @click="GoAgreement(5)" class="foot-bottom__box__content__link">{{ t('footer.icp') }}</a> |
<a @click="GoAgreement(7)" class="foot-bottom__box__content__link">粤公网安备 粤ICP备2023070569号</a> <a @click="GoAgreement(7)" class="foot-bottom__box__content__link">{{ t('footer.public-security') }}</a>
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,12 +1,15 @@
<script setup> <script setup>
import { ref, reactive, onMounted, computed, nextTick, watch } from "vue"; import { ref, reactive, onMounted, computed, nextTick, watch } from "vue";
import { useI18n } from "#imports"; import { useI18n } from "#imports";
import { useRoute } from "#imports"; import { useRoute, useRouter } from "#imports";
import { useNavigation } from "@/composables/useNavigation.js"; import { useNavigation } from "@/composables/useNavigation.js";
import { useLanguageSwitch } from "@/composables/useLanguageSwitch.js";
const { locale, t } = useI18n(); const { locale, t } = useI18n();
const route = useRoute(); const route = useRoute();
const { navigateTo, switchLanguage, localePath } = useNavigation(); const router = useRouter();
const { navigateTo, localePath } = useNavigation();
const { switchLanguage, initLanguage } = useLanguageSwitch();
// //
const langToShow = computed(() => { const langToShow = computed(() => {
@ -23,11 +26,11 @@ const langToShow = computed(() => {
// //
const langList = ref([ const langList = ref([
{ name: "简体中文", code: "cn", checked: false }, { name: "简体中文", code: "cn", checked: false },
{ name: "English", code: "en", checked: true }, { name: "English", code: "en", checked: false },
]); ]);
// // - 使
const menuItems = ref([ const menuItems = computed(() => [
{ {
name: t("nav.home"), name: t("nav.home"),
enName: "home", enName: "home",
@ -54,41 +57,33 @@ const menuItems = ref([
}, },
]); ]);
//
watch(() => locale.value, () => {
menuItems.value.forEach(item => {
switch (item.enName) {
case "home":
item.name = t("nav.home");
break;
case "about":
item.name = t("nav.about");
break;
case "products":
item.name = t("nav.products");
break;
case "contact":
item.name = t("nav.contact");
break;
}
});
}, { immediate: true });
// //
const isMobileMenuOpen = ref(false); const isMobileMenuOpen = ref(false);
const langPopup = ref(false); const langPopup = ref(false);
//
const showLangDropdown = ref(false);
//
const isSwitching = ref(false);
// //
const chooseLanguage = (lang) => { const chooseLanguage = async (lang) => {
if (lang.code === locale.value) { if (lang.code === locale.value || isSwitching.value) {
langPopup.value = false; showLangDropdown.value = false;
return; return;
} }
// 使 try {
switchLanguage(lang.code); isSwitching.value = true;
// 使
langPopup.value = false; await switchLanguage(lang.code, locale, router);
} catch (error) {
console.warn('Language switch failed:', error);
} finally {
isSwitching.value = false;
showLangDropdown.value = false;
}
}; };
// //
@ -102,28 +97,29 @@ const handleMenuClick = (item) => {
const setActiveMenu = () => { const setActiveMenu = () => {
const currentPath = route.path; const currentPath = route.path;
// //
menuItems.value.forEach((item) => { const items = menuItems.value;
items.forEach((item) => {
item.active = false; item.active = false;
}); });
// //
if (currentPath === "/" || currentPath === "/cn") { if (currentPath === "/" || currentPath === "/cn") {
const homeItem = menuItems.value.find((item) => item.enName === "home"); const homeItem = items.find((item) => item.enName === "home");
if (homeItem) { if (homeItem) {
homeItem.active = true; homeItem.active = true;
} }
} else if (currentPath.includes("/about-us")) { } else if (currentPath.includes("/about-us")) {
const aboutItem = menuItems.value.find((item) => item.enName === "about"); const aboutItem = items.find((item) => item.enName === "about");
if (aboutItem) { if (aboutItem) {
aboutItem.active = true; aboutItem.active = true;
} }
} else if (currentPath.includes("/product")) { } else if (currentPath.includes("/product")) {
const productItem = menuItems.value.find((item) => item.enName === "products"); const productItem = items.find((item) => item.enName === "products");
if (productItem) { if (productItem) {
productItem.active = true; productItem.active = true;
} }
} else if (currentPath.includes("/contact-us")) { } else if (currentPath.includes("/contact-us")) {
const contactItem = menuItems.value.find((item) => item.enName === "contact"); const contactItem = items.find((item) => item.enName === "contact");
if (contactItem) { if (contactItem) {
contactItem.active = true; contactItem.active = true;
} }
@ -139,13 +135,13 @@ const toggleMobileMenu = () => {
const handleLogoClick = () => { const handleLogoClick = () => {
// 使 // 使
navigateTo("/"); navigateTo("/");
menuItems.value.forEach((menu) => {
menu.active = menu.enName === "home";
});
isMobileMenuOpen.value = false; isMobileMenuOpen.value = false;
}; };
onMounted(() => { onMounted(() => {
//
initLanguage(locale, router);
// //
langList.value.forEach((item) => { langList.value.forEach((item) => {
item.checked = item.code === locale.value; item.checked = item.code === locale.value;
@ -157,6 +153,13 @@ onMounted(() => {
}); });
}); });
//
watch(() => locale.value, (newLocale) => {
langList.value.forEach((item) => {
item.checked = item.code === newLocale;
});
}, { immediate: true });
// //
watch( watch(
() => route.path, () => route.path,
@ -198,39 +201,51 @@ watch(
<!-- 语言切换和移动端按钮 --> <!-- 语言切换和移动端按钮 -->
<div class="navbar-actions"> <div class="navbar-actions">
<!-- 语言切换 --> <!-- 语言切换 -->
<div class="language-selector" @click="langPopup = !langPopup"> <div
<img class="language-container"
class="lang-earth-icon" @mouseenter="showLangDropdown = true"
src="/static/home/earch.png" @mouseleave="showLangDropdown = false"
:alt="langToShow" @click="showLangDropdown = !showLangDropdown"
:title="langToShow" >
/> <div class="language-selector">
<span class="lang-text">{{ langToShow }}</span> <img
<svg class="lang-earth-icon"
class="lang-icon" src="/static/home/earch.png"
:class="{ rotate: langPopup }" :alt="langToShow"
width="12" :title="langToShow"
height="12"
viewBox="0 0 12 12"
>
<path
d="M2 4l4 4 4-4"
stroke="currentColor"
stroke-width="2"
fill="none"
/> />
</svg> <span class="lang-text">{{ langToShow }}</span>
<svg
class="lang-icon"
:class="{ rotate: showLangDropdown }"
width="12"
height="12"
viewBox="0 0 12 12"
>
<path
d="M2 4l4 4 4-4"
stroke="currentColor"
stroke-width="2"
fill="none"
/>
</svg>
</div>
<!-- 语言下拉菜单 --> <!-- 语言下拉菜单 -->
<div class="lang-dropdown" v-show="langPopup"> <div
class="lang-dropdown"
v-show="showLangDropdown"
@click.stop
>
<div <div
v-for="lang in langList" v-for="lang in langList"
:key="lang.code" :key="lang.code"
class="lang-option" class="lang-option"
:class="{ active: lang.checked }" :class="{ active: lang.checked, disabled: isSwitching }"
@click.stop="chooseLanguage(lang)" @click="chooseLanguage(lang)"
> >
{{ lang.name }} {{ lang.name }}
<span v-if="isSwitching && lang.checked" class="loading-dot">...</span>
</div> </div>
</div> </div>
</div> </div>
@ -291,6 +306,7 @@ watch(
z-index: 1000; z-index: 1000;
transition: all 0.3s ease; transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
pointer-events: auto;
&-container { &-container {
max-width: 1200px; max-width: 1200px;
@ -301,6 +317,7 @@ watch(
padding: 0 20px; padding: 0 20px;
height: 50px; height: 50px;
gap: 60px; gap: 60px;
pointer-events: auto;
/* 4K 和超大屏幕 */ /* 4K 和超大屏幕 */
@media (min-width: 2560px) { @media (min-width: 2560px) {
@ -366,6 +383,7 @@ watch(
display: flex; display: flex;
align-items: center; align-items: center;
gap: 40px; gap: 40px;
pointer-events: auto;
} }
&-menu { &-menu {
@ -380,6 +398,7 @@ watch(
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20px; gap: 20px;
pointer-events: auto;
@media (max-width: 768px) { @media (max-width: 768px) {
gap: 16px; gap: 16px;
@ -452,6 +471,12 @@ watch(
} }
} }
//
.language-container {
position: relative;
z-index: 1001;
}
// //
.language-selector { .language-selector {
position: relative; position: relative;
@ -464,6 +489,8 @@ watch(
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
z-index: 1001; z-index: 1001;
pointer-events: auto;
border-radius: 4px;
&:hover { &:hover {
background: rgba(0, 0, 0, 0.02); background: rgba(0, 0, 0, 0.02);
@ -516,7 +543,7 @@ watch(
position: absolute; position: absolute;
top: 100%; top: 100%;
right: 0; right: 0;
margin-top: 8px; margin-top: 0;
background: white; background: white;
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px; border-radius: 8px;
@ -524,14 +551,21 @@ watch(
overflow: hidden; overflow: hidden;
min-width: 140px; min-width: 140px;
z-index: 1002; z-index: 1002;
pointer-events: auto;
} }
.lang-option { .lang-option {
padding: 10px 14px; padding: 10px 14px;
font-size: 12px; font-size: 12px;
color: #000000; color: #000000;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: space-between;
pointer-events: auto;
&:hover { &:hover {
background: rgba(0, 0, 0, 0.04); background: rgba(0, 0, 0, 0.04);
@ -543,6 +577,23 @@ watch(
color: #000000; color: #000000;
font-weight: 600; font-weight: 600;
} }
&.disabled {
cursor: not-allowed;
opacity: 0.6;
}
.loading-dot {
font-size: 10px;
color: #4f8cef;
animation: loading 1.5s infinite;
}
}
@keyframes loading {
0%, 20% { opacity: 0; }
50% { opacity: 1; }
80%, 100% { opacity: 0; }
} }
// //

View File

@ -0,0 +1,19 @@
import { useLanguageStore } from '@/stores/language.js'
export const useAppInit = () => {
const languageStore = useLanguageStore()
// 应用初始化
const initApp = () => {
console.log('App initialization started')
// 初始化语言
languageStore.initLocale()
console.log('App initialization completed')
}
return {
initApp
}
}

View File

@ -1,79 +0,0 @@
// 统一的语言检测逻辑,确保服务器端和客户端一致
export const useLanguageDetection = () => {
// 检测初始语言的统一逻辑
const detectInitialLanguage = () => {
// 服务端环境
if (typeof window === 'undefined') {
return 'en'; // 服务端默认英文,实际语言由中间件设置
}
// 客户端环境
try {
// 1. 优先使用cookie与服务器中间件保持一致
const cookies = document.cookie.split(';');
const localeCookie = cookies.find(cookie =>
cookie.trim().startsWith('i18n_redirected=')
);
if (localeCookie) {
const langValue = localeCookie.split('=')[1];
if (['cn', 'en'].includes(langValue)) {
return langValue;
}
}
// 2. 检查内联脚本设置的语言
const scriptLang = window.__INITIAL_LANG__ ||
document.documentElement.getAttribute('data-initial-lang');
if (scriptLang && ['cn', 'en'].includes(scriptLang)) {
return scriptLang;
}
// 3. 检查localStorage
const storedLang = localStorage.getItem('locale_lang');
if (storedLang && ['cn', 'en'].includes(storedLang)) {
return storedLang;
}
// 4. 检查浏览器语言(与服务器中间件保持一致)
const browserLang = navigator.language.toLowerCase();
if (browserLang.startsWith("en")) {
return "en";
} else if (browserLang.includes("zh") || browserLang.includes("cn")) {
return "cn";
}
} catch (e) {
console.warn('Language detection failed:', e);
}
return 'en'; // 默认英文
};
// 保存语言设置
const saveLanguage = (lang) => {
if (typeof window === 'undefined') return;
try {
// 保存到Cookie使用Nuxt i18n的标准键名
document.cookie = 'i18n_redirected=' + lang + '; path=/; max-age=31536000';
// 保存到localStorage
localStorage.setItem('locale_lang', lang);
// 更新全局变量
window.__INITIAL_LANG__ = lang;
window.__I18N_INITIAL_LOCALE__ = lang;
// 更新文档属性
document.documentElement.setAttribute('data-initial-lang', lang);
document.documentElement.setAttribute('data-locale', lang);
document.documentElement.setAttribute('lang', lang === 'cn' ? 'zh-CN' : 'en');
} catch (error) {
console.warn('Language save failed:', error);
}
};
return {
detectInitialLanguage,
saveLanguage
};
};

View File

@ -0,0 +1,114 @@
import { useLanguageStore } from '@/stores/language.js'
export const useLanguageSwitch = () => {
const languageStore = useLanguageStore()
// 切换语言 - 简化逻辑,主要依赖 Nuxt i18n 的路由处理
const switchLanguage = async (newLocale, locale, router) => {
if (!newLocale || newLocale === locale.value) {
console.log('Language switch skipped:', { newLocale, current: locale.value })
return
}
console.log('Language switch started:', { from: locale.value, to: newLocale })
try {
// 更新 store 中的语言状态
languageStore.setLocale(newLocale)
console.log('Store updated:', newLocale)
// 更新 i18n locale
locale.value = newLocale
console.log('I18n locale updated:', newLocale)
// 使用 Nuxt i18n 的 switchLocalePath 获取新语言的路径
const currentRoute = router.currentRoute.value
const currentPath = currentRoute.path
console.log('Current path:', currentPath)
// 构建新语言的路径
let newPath = currentPath
if (newLocale === 'cn') {
// 切换到中文,需要添加 /cn 前缀
if (!currentPath.startsWith('/cn')) {
newPath = `/cn${currentPath === '/' ? '' : currentPath}`
}
} else {
// 切换到英文,需要移除 /cn 前缀
if (currentPath.startsWith('/cn')) {
newPath = currentPath.replace('/cn', '') || '/'
}
}
console.log('New path:', newPath)
// 跳转到新语言的对应页面
if (newPath !== currentPath) {
try {
await router.push(newPath)
console.log('Navigation completed')
} catch (error) {
console.warn('Language switch navigation failed:', error)
// 如果路由跳转失败,至少确保语言状态已更新
}
} else {
console.log('No navigation needed')
}
console.log('Language switch completed successfully')
} catch (error) {
console.error('Language switch failed:', error)
// 出错时回退到默认语言
languageStore.setLocale('en')
locale.value = 'en'
}
}
// 获取当前语言显示名称
const getCurrentLanguageName = (locale) => {
try {
const currentLang = languageStore.availableLocales.find(
lang => lang.code === locale.value
)
return currentLang ? currentLang.name : 'English'
} catch (error) {
console.warn('Get language name failed:', error)
return 'English'
}
}
// 初始化语言 - 简化初始化逻辑
const initLanguage = (locale, router) => {
console.log('Language initialization started')
try {
// 确保语言 store 已初始化
languageStore.initLocale()
console.log('Store initialized')
// 同步 store 和 i18n 的语言状态
const storeLocale = languageStore.currentLocale.value
console.log('Store locale:', storeLocale, 'I18n locale:', locale.value)
if (storeLocale && storeLocale !== locale.value) {
locale.value = storeLocale
console.log('I18n locale synced:', storeLocale)
}
console.log('Language initialization completed')
} catch (error) {
console.error('Language initialization failed:', error)
// 出错时使用默认语言
languageStore.setLocale('en')
locale.value = 'en'
}
}
return {
switchLanguage,
getCurrentLanguageName,
initLanguage,
currentLocale: languageStore.currentLocale,
availableLocales: languageStore.availableLocales
}
}

View File

@ -1,5 +1,6 @@
import { useRouter } from '#imports' import { useRouter } from '#imports'
import { useLocalePath, useSwitchLocalePath } from '#i18n' import { useLocalePath, useSwitchLocalePath } from '#i18n'
import { useLanguageSwitch } from './useLanguageSwitch.js'
/** /**
* 统一的导航跳转 composable * 统一的导航跳转 composable
@ -9,6 +10,7 @@ export const useNavigation = () => {
const router = useRouter() const router = useRouter()
const localePath = useLocalePath() const localePath = useLocalePath()
const switchLocalePath = useSwitchLocalePath() const switchLocalePath = useSwitchLocalePath()
const { switchLanguage } = useLanguageSwitch()
/** /**
* 统一的路由跳转方法 * 统一的路由跳转方法
@ -115,10 +117,11 @@ export const useNavigation = () => {
/** /**
* 语言切换跳转 * 语言切换跳转
* @param {string} locale - 目标语言代码 * @param {string} locale - 目标语言代码
* @param {object} i18nLocale - i18n locale 对象
*/ */
const switchLanguage = (locale) => { const switchLanguageRoute = (locale, i18nLocale) => {
const targetPath = switchLocalePath(locale) // 使用新的语言切换逻辑
router.replace(targetPath) switchLanguage(locale, i18nLocale, router)
} }
/** /**
@ -139,7 +142,7 @@ export const useNavigation = () => {
goContact, goContact,
goPrivacyPolicy, goPrivacyPolicy,
goProductDetail, goProductDetail,
switchLanguage, switchLanguage: switchLanguageRoute,
getLocalizedPath, getLocalizedPath,
localePath, localePath,
switchLocalePath switchLocalePath

View File

@ -1,101 +0,0 @@
// export 语言检测 - 改进版支持SSR与Nuxt i18n保持一致
export default function getLanguage() {
// 服务端渲染时的默认语言
if (typeof window === 'undefined') {
return "en";
}
// 客户端逻辑
try {
// 1. 优先检查cookie与Nuxt i18n保持一致
const cookies = document.cookie.split(';');
const localeCookie = cookies.find(cookie =>
cookie.trim().startsWith('i18n_redirected=')
);
if (localeCookie) {
const langValue = localeCookie.split('=')[1];
if (['cn', 'en'].includes(langValue)) {
return langValue;
}
}
// 2. 检查localStorage兼容旧版本
const langStorage = localStorage.getItem('locale_lang');
if (langStorage && ['cn', 'en'].includes(langStorage)) {
return langStorage;
}
// 3. 根据浏览器语言判断
const browserLang = navigator.language.toLowerCase();
if (browserLang.startsWith("en")) {
return "en";
} else if (browserLang.includes("zh") || browserLang.includes("cn")) {
return "cn";
}
return "en"; // 默认英文
} catch (error) {
console.warn('localStorage access failed:', error);
return "en"; // 错误时返回默认语言
}
}
// 新增:用于组件中的响应式语言检测
export async function useLanguage() {
// 检查是否在Nuxt环境中
if (typeof window !== 'undefined') {
// 客户端环境使用useCookie
try {
const { useCookie } = await import('#imports');
return useCookie('i18n_redirected', {
default: () => 'en',
secure: false,
sameSite: 'lax'
});
} catch (e) {
// 如果useCookie不可用回退到普通函数
return { value: getLanguage() };
}
} else {
// 服务端环境,回退到普通函数
return { value: getLanguage() };
}
}
// export 导出防抖函数
export const debounce = (func, delay) => {
let timerId;
return () => {
if (timerId) {
clearTimeout(timerId);
}
timerId = setTimeout(() => {
func();
timerId = null;
}, delay);
};
};
// 时间戳转换年月日
export const formatTimestamp = (timestamp) => {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = date.getMonth() + 1; // getMonth()返回的是0到11所以要加1
const day = date.getDate();
// 将月份和日期补零,确保是两位数
const formattedMonth = month < 10 ? "0" + month : month;
const formattedDay = day < 10 ? "0" + day : day;
return `${year}-${formattedMonth}-${formattedDay}`;
};
// 时间戳转换年月日
export const formatTimestamp2 = (timestamp) => {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0"); // 使用padStart()方法补零
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day}-${hours}:${minutes}`;
};

View File

@ -8,5 +8,8 @@ export default defineI18nConfig(() => ({
messages: { messages: {
en, en,
cn cn
} },
// 移除重复的locale配置让Nuxt i18n自动管理
fallbackWarn: false,
missingWarn: false
})) }))

View File

@ -340,14 +340,17 @@
"nav.products": "产品系列", "nav.products": "产品系列",
"nav.contact": "联系我们", "nav.contact": "联系我们",
"footer.company.name": "中山市明阳良光照明有限公司", "footer.company.name": "中山市明阳良光照明有限公司",
"footer.company.description": "太阳能智能云台摄像头制造商", "footer.company.description": "太阳能智能云台摄像头制造商",
"footer.company.design": "明阳良光原创设计", "footer.company.design": "明阳良光原创设计",
"footer.wechat": "微信", "footer.wechat": "微信",
"footer.whatsapp": "WhatsApp", "footer.whatsapp": "WhatsApp",
"footer.address": "地址", "footer.address": "地址",
"footer.qr.wechat": "微信二维码", "footer.qr.wechat": "微信二维码",
"footer.qr.whatsapp": "WhatsApp二维码", "footer.qr.whatsapp": "WhatsApp二维码",
"footer.copyright": "版权所有 © 2025 明阳良光",
"footer.icp": "粤ICP备2023070569号",
"footer.public-security": "粤公网安备 粤ICP备2023070569号",
"about.exhibition.title": "展会荣誉", "about.exhibition.title": "展会荣誉",
"about.exhibition.subtitle": "国际展会参展经历与企业资质认证展示", "about.exhibition.subtitle": "国际展会参展经历与企业资质认证展示",

View File

@ -347,6 +347,9 @@
"footer.address": "Address", "footer.address": "Address",
"footer.qr.wechat": "WeChat QR Code", "footer.qr.wechat": "WeChat QR Code",
"footer.qr.whatsapp": "WhatsApp QR Code", "footer.qr.whatsapp": "WhatsApp QR Code",
"footer.copyright": "Copyright © 2025 Mingyang Liangguang",
"footer.icp": "ICP No. 2023070569",
"footer.public-security": "Public Security No. 2023070569",
"about.exhibition.title": "Exhibition & Honors", "about.exhibition.title": "Exhibition & Honors",
"about.exhibition.subtitle": "International exhibition experience and corporate qualification certification display", "about.exhibition.subtitle": "International exhibition experience and corporate qualification certification display",
@ -381,13 +384,13 @@
"about.production.title": "Manufacturing Process", "about.production.title": "Manufacturing Process",
"about.production.subtitle": "Professional production team and strict process flow to ensure product quality meets international standards", "about.production.subtitle": "Professional production team and strict process flow to ensure product quality meets international standards",
"about.production.assembly": "Precision Assembly", "about.production.assembly": "Precision Assembly",
"about.production.assembly.desc": "Professional technicians precision assembly", "about.production.assembly.desc": "Professional precision assembly",
"about.production.testing": "Quality Testing", "about.production.testing": "Quality Testing",
"about.production.testing.desc": "Strict testing of every product", "about.production.testing.desc": "Strict product testing",
"about.production.debugging": "Product Debugging", "about.production.debugging": "Product Debugging",
"about.production.debugging.desc": "Ensure optimal product performance", "about.production.debugging.desc": "Ensure optimal performance",
"about.production.packaging": "Exquisite Packaging", "about.production.packaging": "Exquisite Packaging",
"about.production.packaging.desc": "Protect products for safe transportation", "about.production.packaging.desc": "Safe product packaging",
"about.production.quality-control": "Quality Control", "about.production.quality-control": "Quality Control",
"about.production.capacity": "Production Capacity", "about.production.capacity": "Production Capacity",
"about.production.monthly-capacity": "Monthly Capacity (Units)", "about.production.monthly-capacity": "Monthly Capacity (Units)",

View File

@ -14,9 +14,7 @@ const { navigateTo } = useNavigation();
// //
usePageTitle("page.about-us"); usePageTitle("page.about-us");
//
const animatedElements = ref([]);
const isVisible = ref(false);
const goTab = (path) => { const goTab = (path) => {
navigateTo(path); navigateTo(path);

View File

@ -13,9 +13,51 @@ import "swiper/css/navigation";
import "swiper/css/pagination"; import "swiper/css/pagination";
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t, locale } = useI18n();
const { cdnImageUrl } = useImagePath(); const { cdnImageUrl } = useImagePath();
//
onMounted(() => {
console.log('=== 首页语言状态 ===');
console.log('当前语言:', locale.value);
console.log('浏览器语言:', navigator.language);
console.log('浏览器语言列表:', navigator.languages);
// Cookie
const cookieLanguage = document.cookie
.split('; ')
.find(row => row.startsWith('i18n_redirected='))
?.split('=')[1] || '无';
console.log('Cookie 语言:', cookieLanguage);
// Cookie
console.log('所有Cookie:', document.cookie);
// localStorage
console.log('localStorage语言:', localStorage.getItem('i18n_redirected'));
//
console.log('=== 语言检测分析 ===');
console.log('默认语言:', 'en');
console.log('可用语言:', ['en', 'cn']);
console.log('浏览器语言匹配:', navigator.language.includes('zh') ? '应该匹配中文' : '应该匹配英文');
console.log('Cookie应该设置为什么:', navigator.language.includes('zh') ? 'cn' : 'en');
// URL
console.log('当前URL:', window.location.href);
console.log('当前路径:', window.location.pathname);
// Cookiewindow便
window.clearLanguageCookie = () => {
document.cookie = 'i18n_redirected=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
console.log('已清除语言Cookie请刷新页面重新测试');
alert('已清除语言Cookie请刷新页面重新测试');
};
console.log('=== 测试方法 ===');
console.log('在控制台输入: window.clearLanguageCookie() 来清除Cookie');
});
// //
usePageTitle("page.home"); usePageTitle("page.home");

View File

@ -8,23 +8,20 @@
} from "vue-i18n"; } from "vue-i18n";
import zhContent from '~/pages/protocol/components/privacy-policy/zh-page.vue'; import zhContent from '~/pages/protocol/components/privacy-policy/zh-page.vue';
import enContent from '~/pages/protocol/components/privacy-policy/en-page.vue'; import enContent from '~/pages/protocol/components/privacy-policy/en-page.vue';
import langToCheck from '@/hook/lang.js'; import { useLanguageStore } from '@/stores/language.js';
const { const {
t t
} = useI18n(); } = useI18n();
const langIs = ref('') const languageStore = useLanguageStore();
const langIs = ref(languageStore.currentLocale)
useHead({ useHead({
title: t('foot.privacy-policy'), title: t('foot.privacy-policy'),
}) })
onBeforeMount(()=>{
langIs.value = langToCheck()
})
</script> </script>
<template> <template>
<div> <div>
<zh-content v-if="langIs == 'zh-Hans' "></zh-content> <zh-content v-if="langIs == 'cn' "></zh-content>
<ja-content v-else-if="langIs == 'ja' "></ja-content>
<en-content v-else></en-content> <en-content v-else></en-content>
</div> </div>
</template> </template>

View File

@ -11,7 +11,7 @@
GetProductDetailApi GetProductDetailApi
} from "@/service/api.js"; } from "@/service/api.js";
// //
import langToCheck from "@/hook/lang.js"; import { useLanguageStore } from "@/stores/language.js";
import { import {
useI18n useI18n
} from "vue-i18n"; } from "vue-i18n";
@ -28,10 +28,8 @@
t t
} = useI18n(); } = useI18n();
const route = useRoute(); const route = useRoute();
const langIs = ref(''); const languageStore = useLanguageStore();
onBeforeMount(()=>{ const langIs = ref(languageStore.currentLocale);
langIs.value = langToCheck()
})
// //
let proId = ref(""); let proId = ref("");
let proName = ref(""); let proName = ref("");

View File

@ -13,14 +13,15 @@
import { import {
GetProductDetailApi GetProductDetailApi
} from "@/service/api.js"; } from "@/service/api.js";
import langToCheck from "@/hook/lang.js"; import { useLanguageStore } from "@/stores/language.js";
import { import {
useI18n useI18n
} from "vue-i18n"; } from "vue-i18n";
const { const {
t t
} = useI18n(); } = useI18n();
const langIs = ref(''); const languageStore = useLanguageStore();
const langIs = ref(languageStore.currentLocale);
import { import {
useRoute useRoute
} from '#imports'; } from '#imports';
@ -36,7 +37,6 @@
let tabCollection = reactive([]); let tabCollection = reactive([]);
onBeforeMount(() => { onBeforeMount(() => {
langIs.value = langToCheck()
tabCollection.push({ tabCollection.push({
name: t("detail.summary"), name: t("detail.summary"),
value: 0, value: 0,

View File

@ -9,7 +9,7 @@
import { import {
useI18n useI18n
} from "vue-i18n"; } from "vue-i18n";
import langToCheck from "@/hook/lang.js"; import { useLanguageStore } from "@/stores/language.js";
import { useNavigation } from "@/composables/useNavigation.js"; import { useNavigation } from "@/composables/useNavigation.js";
const { const {
@ -20,10 +20,8 @@
title: t('contact.thank-message'), title: t('contact.thank-message'),
}) })
const langIs = ref('') const languageStore = useLanguageStore();
onBeforeMount(()=>{ const langIs = ref(languageStore.currentLocale)
langIs.value = langToCheck()
})
const route = useRoute(); const route = useRoute();
const { goHome } = useNavigation(); const { goHome } = useNavigation();
@ -38,7 +36,7 @@
<img src="/static/contact-us/complete.webp" alt="Icon"> <img src="/static/contact-us/complete.webp" alt="Icon">
</div> </div>
<div class="message">{{t('contact.thank-message')}}</div> <div class="message">{{t('contact.thank-message')}}</div>
<button type="button" @click="BackToMenu">{{$t('contact.back')}}</button> <button type="button" @click="BackToMenu">{{t('contact.back')}}</button>
</div> </div>
</template> </template>

View File

@ -0,0 +1,18 @@
export default defineNuxtPlugin(() => {
// 在客户端初始化时设置语言
if (process.client) {
console.log('=== 语言初始化插件启动 ===')
// 设置全局标记
window.__LANGUAGE_PLUGIN_LOADED__ = true
// 等待 DOM 加载完成后记录状态
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM 加载完成,语言插件就绪')
}, { once: true })
} else {
console.log('DOM 已加载,语言插件就绪')
}
}
})

View File

@ -1,12 +1,39 @@
import langToCheck from "@/hook/lang.js";
import { api } from './config.js'; import { api } from './config.js';
// 获取当前语言
const getCurrentLanguage = () => {
if (process.client) {
try {
// 从 cookie 获取语言设置Nuxt i18n 自动管理)
const cookieLanguage = document.cookie
.split('; ')
.find(row => row.startsWith('i18n_redirected='))
?.split('=')[1];
if (cookieLanguage && ['cn', 'en'].includes(cookieLanguage)) {
return cookieLanguage;
}
// 如果没有 cookie检测浏览器语言
const browserLang = navigator.language?.toLowerCase() || 'en';
if (browserLang.startsWith('zh')) {
return 'cn';
}
return 'en';
} catch (error) {
console.warn('API 服务语言检测失败:', error);
return 'en';
}
}
return 'en'; // 服务端默认英文
};
// 首页轮播图 // 首页轮播图
export const GetCarouseApi = () => { export const GetCarouseApi = () => {
return api("/website/get/homePageCarousel_list", { return api("/website/get/homePageCarousel_list", {
method: 'POST', method: 'POST',
body: { body: {
locale: langToCheck() locale: getCurrentLanguage()
} }
}); });
}; };
@ -16,7 +43,7 @@ export const GetProductCategoryApi = () => {
return api("/website/get/productCategory_list", { return api("/website/get/productCategory_list", {
method: 'POST', method: 'POST',
body: { body: {
locale: langToCheck() locale: getCurrentLanguage()
} }
}); });
}; };
@ -66,7 +93,7 @@ export const GetCertificateApi = () => {
return api("/website/get/certificate_list", { return api("/website/get/certificate_list", {
method: 'POST', method: 'POST',
body: { body: {
locale: langToCheck() locale: getCurrentLanguage()
} }
}); });
}; };
@ -76,7 +103,7 @@ export const GetDownloadApi = () => {
return api("/website/get/appInstallPackage", { return api("/website/get/appInstallPackage", {
method: 'POST', method: 'POST',
body: { body: {
locale: langToCheck() locale: getCurrentLanguage()
} }
}); });
}; };

68
src/stores/language.js Normal file
View File

@ -0,0 +1,68 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useLanguageStore = defineStore('language', () => {
// 当前语言 - 与 Nuxt i18n 同步
const currentLocale = ref('en')
// 可用语言列表
const availableLocales = [
{ code: 'en', name: 'English', nativeName: 'English' },
{ code: 'cn', name: '简体中文', nativeName: '简体中文' }
]
// 计算属性
const isChinese = computed(() => currentLocale.value === 'cn')
const isEnglish = computed(() => currentLocale.value === 'en')
// 设置语言 - 简化逻辑,主要依赖 Nuxt i18n
const setLocale = (locale) => {
if (availableLocales.some(lang => lang.code === locale)) {
currentLocale.value = locale
console.log('Language store updated:', locale)
}
}
// 初始化语言 - 简化初始化逻辑
const initLocale = () => {
if (process.client) {
try {
// 从 cookie 获取语言设置Nuxt i18n 自动管理)
const cookieLanguage = document.cookie
.split('; ')
.find(row => row.startsWith('i18n_redirected='))
?.split('=')[1]
if (cookieLanguage && availableLocales.some(lang => lang.code === cookieLanguage)) {
currentLocale.value = cookieLanguage
console.log('Language initialized from cookie:', cookieLanguage)
return
}
// 如果没有 cookie使用默认语言
currentLocale.value = 'en'
console.log('Using default language: en')
} catch (error) {
console.warn('Language initialization failed:', error)
currentLocale.value = 'en'
}
}
}
// 切换语言 - 简化切换逻辑
const toggleLanguage = () => {
const newLocale = currentLocale.value === 'en' ? 'cn' : 'en'
setLocale(newLocale)
return newLocale
}
return {
currentLocale: computed(() => currentLocale.value),
availableLocales,
isChinese,
isEnglish,
setLocale,
initLocale,
toggleLanguage
}
})