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

View File

@ -4,7 +4,8 @@
"type": "module",
"scripts": {
"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:static": "node scripts/generate-static.js && nuxi generate",
"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) => {
// 服务端中间件,直接执行
const cookie = getCookie(event, 'i18n_redirected');
const acceptLanguage = getHeader(event, 'accept-language') || '';
let detectedLang = 'en'; // 默认英文
// 优先检查 cookie如果有 cookie 就使用 cookie 中的语言
if (cookie && ['cn', 'en'].includes(cookie)) {
detectedLang = cookie;
}
// 如果没有 cookie则根据浏览器语言检测
else if (acceptLanguage.includes('zh')) {
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);
}
}
// 设置基本的国际化响应头
setHeader(event, 'X-Detected-Language', 'en');
// 添加 CORS 支持
setHeader(event, 'Access-Control-Allow-Origin', '*');
setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization');
// 处理 OPTIONS 请求
if (getMethod(event) === 'OPTIONS') {
setResponseStatus(event, 200);
return '';
}
});

View File

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

View File

@ -1,131 +1,16 @@
<script setup name="footer">
import { ref, reactive, onUnmounted, onMounted } from "vue";
import { ref } from "vue";
import { useI18n } from "#imports";
import { GetMessageApi } from "@/service/api.js";
import { useRoute } from "#imports";
import { useNavigation } from "@/composables/useNavigation.js";
const { t } = useI18n();
import { GetProductCategoryApi } from "~/service/api.js";
//
import useSheller from "~/stores/seller.js";
//
import langToCheck from "~/hook/lang.js";
const langIs = ref(langToCheck());
const route = useRoute();
const { navigateTo } = useNavigation();
//
const Store = useSheller();
const props = defineProps({
//
marginSpacing: {
type: Boolean,
default: false,
},
});
import { useLanguageStore } from "@/stores/language.js";
const { t } = useI18n();
const languageStore = useLanguageStore();
const langIs = ref(languageStore.currentLocale);
const { navigateTo } = useNavigation();
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) => {
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>
<template>
@ -234,13 +109,13 @@ const goTab = (path) => {
</div>
</div>
<!-- 末尾 -->
<div class="foot-bottom" v-if="langIs === 'cn'">
<div class="foot-bottom">
<div class="foot-bottom__box">
<div class="foot-bottom__box__content">
<p class="foot-bottom__box__content__copyright">
版权所有 © 2025 明阳良光 |
<a @click="GoAgreement(5)" class="foot-bottom__box__content__link">粤ICP备2023070569号</a> |
<a @click="GoAgreement(7)" class="foot-bottom__box__content__link">粤公网安备 粤ICP备2023070569号</a>
{{ t('footer.copyright') }} |
<a @click="GoAgreement(5)" class="foot-bottom__box__content__link">{{ t('footer.icp') }}</a> |
<a @click="GoAgreement(7)" class="foot-bottom__box__content__link">{{ t('footer.public-security') }}</a>
</p>
</div>
</div>

View File

@ -1,12 +1,15 @@
<script setup>
import { ref, reactive, onMounted, computed, nextTick, watch } from "vue";
import { useI18n } from "#imports";
import { useRoute } from "#imports";
import { useRoute, useRouter } from "#imports";
import { useNavigation } from "@/composables/useNavigation.js";
import { useLanguageSwitch } from "@/composables/useLanguageSwitch.js";
const { locale, t } = useI18n();
const route = useRoute();
const { navigateTo, switchLanguage, localePath } = useNavigation();
const router = useRouter();
const { navigateTo, localePath } = useNavigation();
const { switchLanguage, initLanguage } = useLanguageSwitch();
//
const langToShow = computed(() => {
@ -23,11 +26,11 @@ const langToShow = computed(() => {
//
const langList = ref([
{ 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"),
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 langPopup = ref(false);
//
const showLangDropdown = ref(false);
//
const isSwitching = ref(false);
//
const chooseLanguage = (lang) => {
if (lang.code === locale.value) {
langPopup.value = false;
const chooseLanguage = async (lang) => {
if (lang.code === locale.value || isSwitching.value) {
showLangDropdown.value = false;
return;
}
// 使
switchLanguage(lang.code);
langPopup.value = false;
try {
isSwitching.value = true;
// 使
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 currentPath = route.path;
//
menuItems.value.forEach((item) => {
const items = menuItems.value;
items.forEach((item) => {
item.active = false;
});
//
if (currentPath === "/" || currentPath === "/cn") {
const homeItem = menuItems.value.find((item) => item.enName === "home");
const homeItem = items.find((item) => item.enName === "home");
if (homeItem) {
homeItem.active = true;
}
} 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) {
aboutItem.active = true;
}
} else if (currentPath.includes("/product")) {
const productItem = menuItems.value.find((item) => item.enName === "products");
const productItem = items.find((item) => item.enName === "products");
if (productItem) {
productItem.active = true;
}
} 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) {
contactItem.active = true;
}
@ -139,13 +135,13 @@ const toggleMobileMenu = () => {
const handleLogoClick = () => {
// 使
navigateTo("/");
menuItems.value.forEach((menu) => {
menu.active = menu.enName === "home";
});
isMobileMenuOpen.value = false;
};
onMounted(() => {
//
initLanguage(locale, router);
//
langList.value.forEach((item) => {
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(
() => route.path,
@ -198,39 +201,51 @@ watch(
<!-- 语言切换和移动端按钮 -->
<div class="navbar-actions">
<!-- 语言切换 -->
<div class="language-selector" @click="langPopup = !langPopup">
<img
class="lang-earth-icon"
src="/static/home/earch.png"
:alt="langToShow"
:title="langToShow"
/>
<span class="lang-text">{{ langToShow }}</span>
<svg
class="lang-icon"
:class="{ rotate: langPopup }"
width="12"
height="12"
viewBox="0 0 12 12"
>
<path
d="M2 4l4 4 4-4"
stroke="currentColor"
stroke-width="2"
fill="none"
<div
class="language-container"
@mouseenter="showLangDropdown = true"
@mouseleave="showLangDropdown = false"
@click="showLangDropdown = !showLangDropdown"
>
<div class="language-selector">
<img
class="lang-earth-icon"
src="/static/home/earch.png"
:alt="langToShow"
:title="langToShow"
/>
</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
v-for="lang in langList"
:key="lang.code"
class="lang-option"
:class="{ active: lang.checked }"
@click.stop="chooseLanguage(lang)"
:class="{ active: lang.checked, disabled: isSwitching }"
@click="chooseLanguage(lang)"
>
{{ lang.name }}
<span v-if="isSwitching && lang.checked" class="loading-dot">...</span>
</div>
</div>
</div>
@ -291,6 +306,7 @@ watch(
z-index: 1000;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
pointer-events: auto;
&-container {
max-width: 1200px;
@ -301,6 +317,7 @@ watch(
padding: 0 20px;
height: 50px;
gap: 60px;
pointer-events: auto;
/* 4K 和超大屏幕 */
@media (min-width: 2560px) {
@ -366,6 +383,7 @@ watch(
display: flex;
align-items: center;
gap: 40px;
pointer-events: auto;
}
&-menu {
@ -380,6 +398,7 @@ watch(
display: flex;
align-items: center;
gap: 20px;
pointer-events: auto;
@media (max-width: 768px) {
gap: 16px;
@ -452,6 +471,12 @@ watch(
}
}
//
.language-container {
position: relative;
z-index: 1001;
}
//
.language-selector {
position: relative;
@ -464,6 +489,8 @@ watch(
cursor: pointer;
transition: all 0.3s ease;
z-index: 1001;
pointer-events: auto;
border-radius: 4px;
&:hover {
background: rgba(0, 0, 0, 0.02);
@ -516,7 +543,7 @@ watch(
position: absolute;
top: 100%;
right: 0;
margin-top: 8px;
margin-top: 0;
background: white;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
@ -524,14 +551,21 @@ watch(
overflow: hidden;
min-width: 140px;
z-index: 1002;
pointer-events: auto;
}
.lang-option {
padding: 10px 14px;
font-size: 12px;
color: #000000;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: space-between;
pointer-events: auto;
&:hover {
background: rgba(0, 0, 0, 0.04);
@ -543,6 +577,23 @@ watch(
color: #000000;
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 { useLocalePath, useSwitchLocalePath } from '#i18n'
import { useLanguageSwitch } from './useLanguageSwitch.js'
/**
* 统一的导航跳转 composable
@ -9,6 +10,7 @@ export const useNavigation = () => {
const router = useRouter()
const localePath = useLocalePath()
const switchLocalePath = useSwitchLocalePath()
const { switchLanguage } = useLanguageSwitch()
/**
* 统一的路由跳转方法
@ -115,10 +117,11 @@ export const useNavigation = () => {
/**
* 语言切换跳转
* @param {string} locale - 目标语言代码
* @param {object} i18nLocale - i18n locale 对象
*/
const switchLanguage = (locale) => {
const targetPath = switchLocalePath(locale)
router.replace(targetPath)
const switchLanguageRoute = (locale, i18nLocale) => {
// 使用新的语言切换逻辑
switchLanguage(locale, i18nLocale, router)
}
/**
@ -139,7 +142,7 @@ export const useNavigation = () => {
goContact,
goPrivacyPolicy,
goProductDetail,
switchLanguage,
switchLanguage: switchLanguageRoute,
getLocalizedPath,
localePath,
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: {
en,
cn
}
},
// 移除重复的locale配置让Nuxt i18n自动管理
fallbackWarn: false,
missingWarn: false
}))

View File

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

View File

@ -347,6 +347,9 @@
"footer.address": "Address",
"footer.qr.wechat": "WeChat 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.subtitle": "International exhibition experience and corporate qualification certification display",
@ -381,13 +384,13 @@
"about.production.title": "Manufacturing Process",
"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.desc": "Professional technicians precision assembly",
"about.production.assembly.desc": "Professional precision assembly",
"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.desc": "Ensure optimal product performance",
"about.production.debugging.desc": "Ensure optimal performance",
"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.capacity": "Production Capacity",
"about.production.monthly-capacity": "Monthly Capacity (Units)",

View File

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

View File

@ -13,9 +13,51 @@ import "swiper/css/navigation";
import "swiper/css/pagination";
const router = useRouter();
const { t } = useI18n();
const { t, locale } = useI18n();
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");

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@
import {
useI18n
} from "vue-i18n";
import langToCheck from "@/hook/lang.js";
import { useLanguageStore } from "@/stores/language.js";
import { useNavigation } from "@/composables/useNavigation.js";
const {
@ -20,10 +20,8 @@
title: t('contact.thank-message'),
})
const langIs = ref('')
onBeforeMount(()=>{
langIs.value = langToCheck()
})
const languageStore = useLanguageStore();
const langIs = ref(languageStore.currentLocale)
const route = useRoute();
const { goHome } = useNavigation();
@ -38,7 +36,7 @@
<img src="/static/contact-us/complete.webp" alt="Icon">
</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>
</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';
// 获取当前语言
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 = () => {
return api("/website/get/homePageCarousel_list", {
method: 'POST',
body: {
locale: langToCheck()
locale: getCurrentLanguage()
}
});
};
@ -16,7 +43,7 @@ export const GetProductCategoryApi = () => {
return api("/website/get/productCategory_list", {
method: 'POST',
body: {
locale: langToCheck()
locale: getCurrentLanguage()
}
});
};
@ -66,7 +93,7 @@ export const GetCertificateApi = () => {
return api("/website/get/certificate_list", {
method: 'POST',
body: {
locale: langToCheck()
locale: getCurrentLanguage()
}
});
};
@ -76,7 +103,7 @@ export const GetDownloadApi = () => {
return api("/website/get/appInstallPackage", {
method: 'POST',
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
}
})