都快2026了,还有人不会国际化和暗黑主题适配吗,一篇文章彻底解决

国际化和暗黑主题适配

前端实现国际化和暗黑主题适配已成为现代产品的核心刚需:国际化能够突破语言、地区和文化壁垒,让产品适配全球不同用户的语言习惯、格式规范(日期/货币/地址)及合规要求,覆盖多语言网站、跨境电商、企业级SaaS等场景,是拓展用户群体、提升全球市场竞争力的基础;而暗黑主题适配不仅能在低光环境下保护用户视力、为OLED设备降低功耗,还能适配不同用户的视觉偏好与特殊人群的无障碍需求,同时契合科技、游戏等行业的产品调性,提升用户体验与使用舒适度;二者结合既满足了产品全球化的适配能力,又兼顾了用户个性化的体验诉求,是提升产品包容性、用户留存率和市场适配性的关键设计与开发策略。

1、国际化

国际化(i18n )是指让软件、网站或应用能够适配不同语言、地区和文化习惯的设计与开发过程,其核心目标是无需大规模修改代码 ,就能支持多语言、多地区的本地化部署 使用插件 i18n

1.1 定义存储当前语言的方法

src\utils\cookies.ts

js 复制代码
import Cookies from 'js-cookie'

const languageKey = 'dd_language'
export const getLanguage = () => Cookies.get(languageKey)
export const setLanguage = (language:string) => Cookies.set(languageKey, language)

1.2 准备翻译文件

有的组件国际化语言较少,可能自行添加,比如vxe-table版本不是最新的,没有越南和俄罗斯翻译,需手动添加。

1.3 翻译文件

1.3.1 主文件内容
  • src\lang\index.ts
  • 注释已经很详细了,就是对翻译文件进行合并,然后根据当前选中的语言进行应用
  • 重点介绍一下远程翻译,很多时候翻译的不是很准确,客户可能需要能自行进行翻译,我们可以在这里定义一个 loadRemoteMessages 方法,用于加载远程翻译,后端返回给你远程自定义的翻译,然后通过扩展运算符进行合并,后面的数据会覆盖前面,从而实现替换,非常简单吧,根本没啥难度。
  • document.title 能直接修改网页标题,可以通过这个设置网页标题多语言。
js 复制代码
import { createI18n } from 'vue-i18n';
import { getLanguage } from '../utils/cookies';
// import { fetchRemoteTranslations } from '../api/translation' // 引入远程接口

// vxe-table 组件翻译(保留本地,不被远程覆盖)
import vxe_zhCN from 'vxe-table/lib/locale/lang/zh-CN';
import vxe_enUS from 'vxe-table/lib/locale/lang/en-US';
import vxe_ruRU from '@/lang/vxeTabel/ru_RU';


// ant-design 组件翻译(保留本地,不被远程覆盖)
import ant_zhCN from 'ant-design-vue/lib/locale/zh_CN';
import ant_enUS from 'ant-design-vue/lib/locale/en_US';
import ant_ruRU from 'ant-design-vue/lib/locale/ru_RU';


// dayjs 本地化(无需修改)
import "dayjs/locale/zh-cn";
import "dayjs/locale/en";
import "dayjs/locale/ru";

// 本地业务翻译(可能被远程覆盖)
import enLocale from './en';
import zhLocale from './zh';
import ruLocale from './ru';



// 初始化本地消息(组件库翻译 + 本地业务翻译)
const baseMessages = {
    en_US: {
        ...ant_enUS,    // 组件库翻译(优先级最高,不被覆盖)
        ...vxe_enUS,
        ...enLocale     // 本地业务翻译(可能被远程覆盖)
    },
    zh_CN: {
        ...ant_zhCN,
        ...vxe_zhCN,
        ...zhLocale
    },
    ru_RU: {
        ...ant_ruRU,
        ...vxe_ruRU,
        ...ruLocale
    }
}

// 获取当前语言(复用你的现有逻辑)
export const getLocale = () => {
    const cookieLanguage = getLanguage()
    if (cookieLanguage) {
        document.documentElement.lang = cookieLanguage
        return cookieLanguage
    }
    const language = navigator.language.toLowerCase()
    const locales = Object.keys(baseMessages)
    for (const locale of locales) {
        if (language.indexOf(locale) > -1) {
            document.documentElement.lang = locale
            return locale
        }
    }
    return 'zh_CN' // 默认中文
}

// 创建基础 i18n 实例(先加载本地翻译)
const i18n = createI18n({
    legacy: false,
    globalInjection: true,
    locale: getLocale(),
    messages: baseMessages, // 先使用本地基础翻译
    silentTranslationWarn: true
})

// 核心:加载远程翻译并合并(远程覆盖本地业务翻译,不影响组件库翻译)
// 定义语言类型
type Locale = 'en_US' | 'zh_CN' | 'ru_RU';

// 修改 loadRemoteMessages 函数,添加类型约束
export const loadRemoteMessages = async (locale: Locale = getLocale()) => {
    try {
        // const remoteTrans = await fetchRemoteTranslations(locale)
        let remoteTrans = {
            99001: "测试远程翻译",
        }
        document.title = '波奇壁纸管理系统'
        if (locale == 'en_US') {
            remoteTrans = {
                99001: "test this a remote translation",
            }
            document.title = 'bocchi wallpaper management system'
        }
        // 2. 合并规则:远程翻译覆盖本地业务翻译,但不影响组件库翻译
        const merged = {
            ...baseMessages[locale], // 基础:组件库 + 本地业务
            ...remoteTrans          // 远程业务翻译覆盖本地业务
        }
        // 3. 更新 i18n 实例的翻译数据
        i18n.global.setLocaleMessage(locale, merged)
        console.log(`已加载 ${locale} 远程翻译`)
    } catch (err) {
        console.error(`远程翻译加载失败,使用本地翻译:`, err)
        // 失败时仍使用本地翻译
    }
}


// 初始化时立即加载远程翻译(确保页面首次渲染使用最新翻译)
loadRemoteMessages()

export default i18n
1.3.1 翻译文件示例

src\lang\en.ts

js 复制代码
export default {
    // 系统 10
    10001: '',

    // 菜单 20
    20001: 'Home',
    20002: 'Wallpaper Management',
    20003: 'Wallpaper Information',
    20004: 'Image Upload',
    20005: 'Video Upload',
    20006: 'Data Management',
    20007: 'User Management',
    20008: 'User Information',
    20009: 'Long-term Inactive',
    20010: 'Entertainment Management',
    20011: 'System Management',
    20012: 'Port Information',
    20013: 'Software Information'
}

引用并挂载多语言 src\main.ts

js 复制代码
import { createApp } from 'vue'
import App from '@/App.vue'
import 'ant-design-vue/dist/reset.css'
import '@/styles/global.css'
import '@/styles/vxeTable.css'
import router from './router'
import VXETable from 'vxe-table'
import store from './store'
import 'vxe-table/lib/style.css'
import '@/router/permisstion'
import globalComponents from '@/components/index'
import globalDirective from '@/directives/index'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import './auto-update' // 自动更新
import i18n from './lang'

VXETable.setConfig({
  size: 'mini', // 全局尺寸
  i18n: (key: string, args: any) => i18n.global.t(key, args),
  translate(key, args) {
    return i18n.global.t(key, args)
  }
})

const app = createApp(App)
// @ts-ignore
app.config.globalProperties.$i18n = i18n;

app.use(i18n) 
app.use(router)
app.use(VXETable)
store.use(piniaPluginPersistedstate)
app.use(store)
app.use(globalComponents)
app.use(globalDirective)
app.mount('#app')

1.4 定义多语言切换组件

  • src\components\layout\Language\index.vue
  • 在合适的地方引入即可实现多语言切换
js 复制代码
<template>
    <div class="Language">
        <a-dropdown>
            <template #overlay>
                <a-menu @click="onLanguage">
                    <a-menu-item v-for="itm in languageList" :key="itm.value" :name="itm.label">
                        {{ itm.label }}
                    </a-menu-item>
                </a-menu>
            </template>
            <a-button style="width: 110px">
                {{ language }}
                <DownOutlined />
            </a-button>
        </a-dropdown>
    </div>
</template>
<script lang="ts" setup>
import { computed, onMounted } from 'vue';
import { DownOutlined } from '@ant-design/icons-vue';
import { useI18n } from "vue-i18n";
import { useUserStore } from '@/store/user'
import { storeToRefs } from 'pinia'
import { setLanguage } from '@/utils/cookies'
import { loadRemoteMessages } from '@/lang/index'

const { locale } = useI18n();
const store = useUserStore()
const { lang } = storeToRefs(store);

const emit = defineEmits(['on-click'])

//多语音
const language = computed(() => {
    switch (lang.value) {
        case 'zh_CN':
            return '简体中文';
        case 'en_US':
            return 'English';
        case 'ru_RU':
            return 'Russia';
        default:
            return '简体中文';
    }
})
const languageList = [
    { label: '简体中文', value: 'zh_CN' },
    { label: 'English', value: 'en_US' },
    { label: 'Russia', value: 'ru_RU' },
]

//切换语言
const onLanguage = async (v: any) => {
    // language.value = v.item.name;
    await loadRemoteMessages(v.key);
    locale.value = v.key;
    lang.value = v.key;
    setLanguage(v.key)
    emit('on-click')
}

onMounted(() => {
    setLanguage(lang.value)
})
</script>

<style scoped lang="scss"></style>

1.5 主UI库适配

  • src\App.vue
  • 这里以 ant-design-vue 为例,按照官网方法设置即可,其它 UI库同理。
js 复制代码
<template>
  <ConfigProvider :locale="language" :theme="themeConfig">
    <router-view />
  </ConfigProvider>
</template>

<script setup lang="ts">
import { computed, ref, watch, onMounted } from 'vue'
import 'dayjs/locale/zh-cn'
import { useUserStore } from '@/store/user.ts'
import themeStore from '@/store/themeStore'
import { storeToRefs } from 'pinia'
import zhCN from 'ant-design-vue/lib/locale/zh_CN'
import enUS from 'ant-design-vue/lib/locale/en_US'
import ruRU from 'ant-design-vue/lib/locale/ru_RU'
import { ConfigProvider, theme } from "ant-design-vue";

const userStore = useUserStore()
const { lang } = storeToRefs(userStore)
const { isDark } = storeToRefs(themeStore())

const themeConfig = ref({
  algorithm: isDark.value ? theme.darkAlgorithm : theme.defaultAlgorithm, // 核心:切换明暗算法
});

watch(isDark, (newVal) => {
  themeConfig.value.algorithm = newVal ? theme.darkAlgorithm : theme.defaultAlgorithm
  document.documentElement.classList.toggle('dark', newVal);
})


const obj: any = {
  'zh_CN': zhCN,
  'en_US': enUS,
  'ru_RU': ruRU
}

const language = computed(() => {
  return obj[lang.value]
})

onMounted(() => {
  document.documentElement.classList.toggle('dark', isDark.value);
})

</script>

<style lang="scss"></style>

1.6 使用

直接使用 $t('key') 或者 引入后使用 t('key') 都行

vue 复制代码
<template>
    <div>
        <hr>
        {{ t('99001') }}
        <hr>
        {{ $t('99001') }}
        <hr>
    </div>
</template>

<script lang="ts" setup>
import { useI18n } from "vue-i18n";

const { t } = useI18n();
</script>

1.7 效果预览

2、暗黑模式

暗黑模式(Dark Mode)核心价值是适配特定行业调性。刚做的工厂mes系统,现场属于高亮环境,不同时间段还会有太阳光反射等情况,亮色模式下看不清,领导就要求加入暗黑模式,咱就给他加。

2.1 定义主题仓库

  • src\store\themeStore.ts
  • 用于记录主题和改变主题
  • 仓库已做持久化存储
js 复制代码
import { defineStore } from 'pinia'

const themeStore = defineStore('theme', {
  state: (): {
    isDark: boolean,
  } => {
    return {
      isDark: false,
    }
  },
  persist: true,
  actions: {
    toggleTheme() {
      this.isDark = !this.isDark
    },
  },
  getters: {},
})

export default themeStore

2.2 定义主题参数

  • src\styles\global.css
2.2.1自定义 css 全局变量
  • 分别定义 暗黑主题 和 亮色主题 的css变量
css 复制代码
/* 基础变量:覆盖 90% 通用场景,与 Antd 风格对齐 */
:root {
    --bg: white;
    /* ========== 背景色 ========== */
    --bg-primary: #ffffff;
    --bg-secondary: #f5f5f5;
    --bg-tertiary: #fafafa;
    --bg-overlay: #ffffff;

    /* ========== 文字色 ========== */
    --text-primary: black;
    --text-secondary: #4b5563;
    --text-tertiary: #9ca3af;
    --text-inverse: #ffffff;

    /* ========== 边框色 ========== */
    --border-primary: #e5e7eb;
    --border-secondary: #d1d5db;
    --border-hover: #9ca3af;

    /* ========== 功能色(与 Antd 主色对齐) ========== */
    --color-primary: #1890ff;
    --color-success: #52c41a;
    --color-warning: #faad14;
    --color-danger: #ff4d4f;
    --color-info: #1890ff;

    /* ========== 交互状态 ========== */
    --hover-bg: #f9fafb;
    --active-bg: #f3f4f6;
    --disabled-bg: #f9fafb;

    /* 偏白色的主色调 */
    --color-primary-light: #e6f7ff;
    --color-primary-lighter: #bae7ff;
    --color-primary-lightest: #69c0ff;
    --color-primary-darker: #40a9ff;
    --color-primary-darkest: #096dd9;

    /* 白色透明梯度色 */
    --color-gradient-light: rgba(255, 255, 255, 0.3);
    --color-gradient-lighter: rgba(255, 255, 255, 0.4);
    --color-gradient-lightest: rgba(255, 255, 255, 0.5);

    /* 表格相关 */
    --table-hover-bg: #f1f2ffe8;
    --table-active-bg: #cee9ffe2;



    /* vxe-table 专属变量 */
    --vxe-table-bg: var(--bg-primary);
    --vxe-table-header-bg: var(--bg-secondary);
    --vxe-table-row-hover-bg: #f9fafb;
    --vxe-table-row-active-bg: #f3f4f6;
    --vxe-table-border-color: var(--border-primary);
    --vxe-table-text-color: var(--text-primary);
    --vxe-table-header-text-color: var(--text-primary);
    --vxe-table-cell-hover-color: var(--color-primary);


}

/* 暗黑模式变量:基于 Antd 暗黑风格优化 */
.dark {
    --bg: black;
    /* ========== 背景色 ========== */
    --bg-primary: #141414;
    --bg-secondary: #1f1f1f;
    --bg-tertiary: #272727;
    --bg-overlay: #1f1f1f;

    /* ========== 文字色 ========== */
    --text-primary: white;
    --text-secondary: #9ca3af;
    --text-tertiary: #6b7280;
    --text-inverse: #141414;

    /* ========== 边框色 ========== */
    --border-primary: #303030;
    --border-secondary: #3d3d3d;
    --border-hover: #474747;

    /* ========== 功能色(暗黑模式优化) ========== */
    --color-primary: #359eff;
    --color-success: #67c23a;
    --color-warning: #feb019;
    --color-danger: #ff7875;
    --color-info: #359eff;

    /* ========== 交互状态 ========== */
    --hover-bg: #272727;
    --active-bg: #303030;
    --disabled-bg: #1f1f1f;

    /* 暗黑模式 - 偏暗的主色调(匹配原亮色蓝色系,适配深色背景) */
    --color-primary-light: #142f40;
    --color-primary-lighter: #103c58;
    --color-primary-lightest: #2386c8;
    --color-primary-darker: #2994e0;
    --color-primary-darkest: #359eff;

    /* 黑色透明梯度色 */
    --color-gradient-light: rgba(0, 0, 0, 0.5);
    --color-gradient-lighter: rgba(0, 0, 0, 0.6);
    --color-gradient-lightest: rgba(0, 0, 0, 0.7);

    /* 表格相关 */
    --table-hover-bg: #4e4e4e;
    --table-active-bg: #000a3aa4;


    /* vxe-table 专属变量(适配暗黑) */
    --vxe-table-bg: var(--bg-primary);
    --vxe-table-header-bg: var(--bg-secondary);
    --vxe-table-row-hover-bg: #272727;
    --vxe-table-row-active-bg: #303030;
    --vxe-table-border-color: var(--border-primary);
    --vxe-table-text-color: var(--text-primary);
    --vxe-table-header-text-color: var(--text-primary);
    --vxe-table-cell-hover-color: var(--color-primary);
}
2.2.2 覆盖插件样式
  • 对于一些没有适配暗黑模式或者暗黑模式不清晰的插件,可以对样式进行覆盖
  • 这里以 vxe-table 为例,官网的设置暗黑方法不存在,可能是版本原因吧
  • 其它插件同理覆盖即可
css 复制代码
/* 覆盖 vxe-table 基础样式 - 全局生效 */
.vxe-table {
    --vxe-font-color: var(--vxe-table-text-color) !important;
    --vxe-table-header-font-color: var(--vxe-table-header-text-color) !important;
    --vxe-table-row-hover-bg-color: var(--vxe-table-row-hover-bg) !important;
    --vxe-table-row-current-bg-color: var(--vxe-table-row-active-bg) !important;
    --vxe-table-row-hover-font-color: var(--vxe-table-text-color) !important;
    --vxe-border-color: var(--vxe-table-border-color) !important;
    --vxe-table-header-bg-color: var(--vxe-table-header-bg) !important;
    --vxe-table-body-bg-color: var(--vxe-table-bg) !important;
    --vxe-table-empty-bg-color: var(--vxe-table-bg) !important;
    --vxe-input-border-color: var(--vxe-table-border-color) !important;
    --vxe-input-bg-color: var(--vxe-table-bg) !important;
    --vxe-input-font-color: var(--vxe-table-text-color) !important;
    --vxe-border-color: var(--border-primary) !important;
    --vxe-table-border-color: var(--border-primary) !important;
}


.vxe-header--row {
    background-color: var(--bg-primary) !important;
}

.row--level-0 {
    background-color: var(--bg-secondary) !important;
}

.row--stripe {
    background-color: var(--bg-tertiary) !important;
}


.vxe-pager {
    background-color: var(--bg-primary) !important;
}

.vxe-loading {
    background-color: var(--color-gradient-light) !important;
}

.vxe-input--inner,
.vxe-pager--jump-prev,
.vxe-select--panel-wrapper,
.vxe-pager--prev-btn,
.vxe-pager--num-btn,
.vxe-pager--next-btn,
.vxe-pager--jump-next,
.vxe-pager--goto,
.vxe-pager {
    background-color: var(--bg-secondary) !important;
    border-color: var(--border-primary) !important;
    color: var(--text-secondary) !important;
}


/* 进入行 */
.row--hover {
    background-color: var(--table-hover-bg) !important;
}

/* 选中行 */
.row--current {
    background-color: var(--table-active-bg) !important;
}


.vxe-table--body-wrapper {
    background-color: var(--bg-secondary) !important;
}


.vxe-toolbar {
    background-color: var(--bg-primary) !important;
    border-color: var(--border-primary) !important;
    color: var(--text-secondary) !important;
}

/*滚动条整体部分*/
.vxe-table ::-webkit-scrollbar {
    width: 8px !important;
    height: 8px !important;
}

/*滚动条的轨道*/
.vxe-table ::-webkit-scrollbar-track {
    background: transparent !important;
}

/*滚动条里面的小方块,能向上向下移动*/
.vxe-table ::-webkit-scrollbar-thumb {
    background-color: var(--bg-secondary) !important;
    border-radius: 5px !important;
    border: 1px solid var(--border-primary) !important;
    box-shadow: inset 0 0 6px var(--text-secondary) !important;
}

/*边角,即两个滚动条的交汇处*/
.vxe-table ::-webkit-scrollbar-corner {
    background-color: var(--bg-secondary) !important;
}

.vxe-pager--btn-wrapper .is--active {
    background-color: var(--color-primary) !important;
    color: white !important;
}

/* 覆盖 vxe-table 基础样式 - 全局生效 */
.vxe-table {
    --vxe-font-color: var(--vxe-table-text-color) !important;
    --vxe-table-header-font-color: var(--vxe-table-header-text-color) !important;
    --vxe-table-row-hover-bg-color: var(--vxe-table-row-hover-bg) !important;
    --vxe-table-row-current-bg-color: var(--vxe-table-row-active-bg) !important;
    --vxe-table-row-hover-font-color: var(--vxe-table-text-color) !important;
    --vxe-border-color: var(--vxe-table-border-color) !important;
    --vxe-table-header-bg-color: var(--vxe-table-header-bg) !important;
    --vxe-table-body-bg-color: var(--vxe-table-bg) !important;
    --vxe-table-empty-bg-color: var(--vxe-table-bg) !important;
    --vxe-input-border-color: var(--vxe-table-border-color) !important;
    --vxe-input-bg-color: var(--vxe-table-bg) !important;
    --vxe-input-font-color: var(--vxe-table-text-color) !important;
    --vxe-border-color: var(--border-primary) !important;
    --vxe-table-border-color: var(--border-primary) !important;
}


.vxe-header--row {
    background-color: var(--bg-primary) !important;
}

.row--level-0 {
    background-color: var(--bg-secondary) !important;
}

.row--stripe {
    background-color: var(--bg-tertiary) !important;
}


.vxe-pager {
    background-color: var(--bg-primary) !important;
}

.vxe-loading {
    background-color: var(--color-gradient-light) !important;
}

.vxe-input--inner,
.vxe-pager--jump-prev,
.vxe-select--panel-wrapper,
.vxe-pager--prev-btn,
.vxe-pager--num-btn,
.vxe-pager--next-btn,
.vxe-pager--jump-next,
.vxe-pager--goto,
.vxe-pager {
    background-color: var(--bg-secondary) !important;
    border-color: var(--border-primary) !important;
    color: var(--text-secondary) !important;
}


/* 进入行 */
.row--hover {
    background-color: var(--table-hover-bg) !important;
}

/* 选中行 */
.row--current {
    background-color: var(--table-active-bg) !important;
}


.vxe-table--body-wrapper {
    background-color: var(--bg-secondary) !important;
}


.vxe-toolbar {
    background-color: var(--bg-primary) !important;
    border-color: var(--border-primary) !important;
    color: var(--text-secondary) !important;
}

/*滚动条整体部分*/
.vxe-table ::-webkit-scrollbar {
    width: 8px !important;
    height: 8px !important;
}

/*滚动条的轨道*/
.vxe-table ::-webkit-scrollbar-track {
    background: transparent !important;
}

/*滚动条里面的小方块,能向上向下移动*/
.vxe-table ::-webkit-scrollbar-thumb {
    background-color: var(--bg-secondary) !important;
    border-radius: 5px !important;
    border: 1px solid var(--border-primary) !important;
    box-shadow: inset 0 0 6px var(--text-secondary) !important;
}

/*边角,即两个滚动条的交汇处*/
.vxe-table ::-webkit-scrollbar-corner {
    background-color: var(--bg-secondary) !important;
}

.vxe-pager--btn-wrapper .is--active {
    background-color: var(--color-primary) !important;
    color: white !important;
}


.vxe-icon-checkbox-unchecked {
    color: var(--text-tertiary) !important;
}

2.3 引入定义好的变量和样式

  • src\main.ts
js 复制代码
import { createApp } from 'vue'
import App from '@/App.vue'
import 'ant-design-vue/dist/reset.css'
import '@/styles/global.css'
import '@/styles/vxeTable.css'
import router from './router'
import VXETable from 'vxe-table'
import store from './store'
import 'vxe-table/lib/style.css'
import '@/router/permisstion'
import globalComponents from '@/components/index'
import globalDirective from '@/directives/index'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import './auto-update' // 自动更新
import i18n from './lang'

VXETable.setConfig({
  size: 'mini', // 全局尺寸
  i18n: (key: string, args: any) => i18n.global.t(key, args),
  translate(key, args) {
    return i18n.global.t(key, args)
  }
})

const app = createApp(App)
// @ts-ignore
app.config.globalProperties.$i18n = i18n;

app.use(i18n) 
app.use(router)
app.use(VXETable)
store.use(piniaPluginPersistedstate)
app.use(store)
app.use(globalComponents)
app.use(globalDirective)
app.mount('#app')

2.4 定义主题切换组件

  • src\components\layout\theme\index.vue
  • 在合适的地方引入即可实现主题切换
js 复制代码
<template>
        <a-button class="themeButton"  @click="toggleDark">
            <img :src="isDark ? dark : light" alt="" style="width: 20px; height: 20px">
        </a-button>
</template>

<script setup lang="ts">
import themeStore from '@/store/themeStore'
import { storeToRefs } from 'pinia'
import dark from '@/assets/images/dark.png'
import light from '@/assets/images/light.png'

const themes = themeStore()
const { isDark } = storeToRefs(themes)

const toggleDark = () => {
    isDark.value = !isDark.value;
};
</script>

<style scoped lang="scss">
.themeButton{
    cursor: pointer;
    height: 32px;
    width: 32px;
    box-sizing: border-box;
    border: 1px solid var(--border-primary);
    border-radius: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
}
</style>

2.5 主UI库适配、应用主题

  • src\App.vue
  • 这里以 ant-design-vue 为例,按照官网方法设置即可,其它 UI库同理。
  • 原理很简单,就是监听是否是暗黑主题,如果是则为根元素添加 dark 类名,不是则移除。
  • 记得初始化时默认设置一次类名
js 复制代码
<template>
  <ConfigProvider :locale="language" :theme="themeConfig">
    <router-view />
  </ConfigProvider>
</template>

<script setup lang="ts">
import { computed, ref, watch, onMounted } from 'vue'
import 'dayjs/locale/zh-cn'
import { useUserStore } from '@/store/user.ts'
import themeStore from '@/store/themeStore'
import { storeToRefs } from 'pinia'
import zhCN from 'ant-design-vue/lib/locale/zh_CN'
import enUS from 'ant-design-vue/lib/locale/en_US'
import ruRU from 'ant-design-vue/lib/locale/ru_RU'
import { ConfigProvider, theme } from "ant-design-vue";

const userStore = useUserStore()
const { lang } = storeToRefs(userStore)
const { isDark } = storeToRefs(themeStore())

const themeConfig = ref({
  algorithm: isDark.value ? theme.darkAlgorithm : theme.defaultAlgorithm, // 核心:切换明暗算法
});

watch(isDark, (newVal) => {
  themeConfig.value.algorithm = newVal ? theme.darkAlgorithm : theme.defaultAlgorithm
  document.documentElement.classList.toggle('dark', newVal);
})


const obj: any = {
  'zh_CN': zhCN,
  'en_US': enUS,
  'ru_RU': ruRU
}

const language = computed(() => {
  return obj[lang.value]
})

onMounted(() => {
  document.documentElement.classList.toggle('dark', isDark.value);
})

</script>

<style lang="scss"></style>

2.6 使用

  • 使用就非常简单了,通过 var() 使用自定义的css变量即可。
css 复制代码
.form{
    background-color: var(--bg-primary);
    color: var(--text-primary);
}

2.7 效果预览

  • 效果非常明显,暗黑模式在高亮环境下依然清晰可见。

3、测试项目地址

gitee.com/zsnoin-can/...

相关推荐
两个西柚呀1 小时前
es6和commonjs模块化规范的深入理解
前端·javascript·es6
www_stdio1 小时前
爬楼梯?不,你在攀登算法的珠穆朗玛峰!
前端·javascript·面试
爱吃大芒果1 小时前
Flutter 表单开发实战:表单验证、输入格式化与提交处理
开发语言·javascript·flutter·华为·harmonyos
光影少年1 小时前
RN vs Flutter vs Expo 选型
前端·flutter·react native
风止何安啊2 小时前
🚀别再卷 Redux 了!Zustand 才是 React 状态管理的躺平神器
前端·react.js·面试
鹿角片ljp2 小时前
Spring Boot Web入门:从零开始构建web程序
前端·spring boot·后端
向下的大树2 小时前
Vue 2迁移Vue 3实战:从痛点到突破
前端·javascript·vue.js
我很苦涩的2 小时前
原生小程序使用echarts
前端·小程序·echarts
玉米Yvmi2 小时前
从零理解 CSS 弹性布局:轻松掌控页面元素排布
前端·javascript·css