🔥UniApp 仅需 5 行代码!实现所有页面中控制应用主题变化

1 阅读4分钟

书接上文。上次讲了 oiyo 在 App.vue 赋予的能力,不少朋友说想看实际例子,光说概念不够直观。

行,今天拿 UniApp 最常见的痛点之一来抛砖引玉:多主题色变更

QQ20260623-145924-HD.gif

1. 赋能差异

众所周知,小程序侧在设计上就没有 "根部视图" 这一层概念。导致 UniApp 的 App.vue 只承担生命周期 hook,不参与渲染。你在 <template> 里写再多结构,UniApp 也不会把它呈现给用户。

先感受一下 oiyo 赋能之间的差异

未赋能前

image.png

你在原生 UniApp 里能做什么:

  • 在 App.vue 里放 <ConfigProvider> 统一控制主题?

    • 不行,没有渲染根节点。
  • 在 App.vue 写 <template> 节点让所有页面共享?

    • 只能靠复制代码到每个页面。
  • 在 App.vue 定义一个可以被 A、B、C 页面共同控制变量?

    • 不能,只能承担部分应用级功能。

这就是上图这种 "每个页面重复定义" 的根源。

被赋能后

image.png 你使用 Oiyo 赋能后能够做到:

  • App.vue 是真正的根部视图,主题状态定义一次,所有页面共享。
  • ConfigProvider 包裹全应用,一个应用级共享 themeVars 变化全域响应。
  • 页面 A 主动切换了主题色,页面B以及其他页面均立刻感知到变化。

这种赋能方式解决了过去在 UniApp 里很难做到的跨页面状态共享问题。

2. 用 30 秒创建验证项目

概念层面已经清楚 Oiyo 能解决什么问题了。如果你现在就想在真实项目里跑通主题切换,先花 30 秒搭一个 Oiyo 项目。

pnpm create oiyo@latest --preset=standard

脚手架会交互式引导你指定安装位置,用于创建一份 包含 wot-ui v2 等基础功能 的最佳实践项目:

image.png

别忘记进行项目安装,按照脚手架提示的 “下一步” 引导就可以正常安装。

现在你有了一个具备 oiyo + wot-ui 组件库的可用项目。接下来在这个项目中实现主题切换。

3. 两步实现全局主题切换

前提都具备后,接入流程变成一条直线。

第一步:在 App.vue 中组合根部视图

<!-- 入口 src/App.vue 的文件 -->
<script setup lang="ts">
const { isShowThemeSheet, toggleThemeSheet, themeOptions, currentTheme, currentThemeLabel } = defineRootContext(() => {
  const isShowThemeSheet = ref(false)
  const toggleThemeSheet = () => isShowThemeSheet.value = !isShowThemeSheet.value

  const themeOptions = ref([
    { label: 'Shadcn', value: 'shadcn' },
    { label: 'NutUI', value: 'nutui' },
    { label: 'Vant', value: 'vant' },
    { label: 'TDesign', value: 'tdesign' },
    { label: 'Cartoon', value: 'cartoon' },
    { label: 'Illustration', value: 'illustration' },
  ])

  const currentTheme = ref(['shadcn'])

  const currentThemeLabel = computed(() => {
    return themeOptions.value.find(item => item.value === currentTheme.value[0])?.label
  })

  return { isShowThemeSheet, toggleThemeSheet, themeOptions, currentTheme, currentThemeLabel }
})
</script>

<template>
  <WdConfigProvider :theme="currentTheme[0]">
    <view class="wrapper relative wot-bg-filled-bottom">
      <WdCell title="主题预设" is-link :value="currentThemeLabel" custom-class="sticky! top-[var(--window-top)] z-10" @click="toggleThemeSheet" />

      <OiyoLayout>
        <OiyoPage />
      </OiyoLayout>

      <WdPicker v-model="currentTheme" v-model:visible="isShowThemeSheet" :columns="themeOptions" />
    </view>
  </WdConfigProvider>
</template>

<style lang="scss">
@use '@wot-ui/ui/styles/theme/index.scss' as *;
@use './themes/presets.scss' as *; // 由于这文件太长,需要时可以从这里获取:https://github.com/wot-ui/wot-ui/blob/main/src/theme/presets.scss

.wrapper {
  min-height: calc(100vh - var(--window-top) - var(--window-bottom));
  box-sizing: border-box;
}
</style>

这一步同时用到了 Oiyo 的两项能力:<template> 渲染让 <WdConfigProvider> 有了放置位置,defineRootContext 让主题变化可以从根部扩散到所有页面。

第二步:在页面中放入感受直观的 UI 组件

<!-- 页面 src/pages/home/index.vue 的文件 -->
<script setup lang="ts">
definePageMeta({
  type: 'home',
  style: {
    navigationBarTitleText: '首页',
  },
})

const switchValue = ref(true)
const checkboxValue = ref(true)
const radioValue = ref('1')
const inputValue = ref('')
</script>

<template>
  <view class="p-3">
    <!-- 按钮 -->
    <WdCellGroup title="按钮 Button" border>
      <WdCell title-width="0" center>
        <view class="flex flex-wrap gap-2">
          <WdButton type="primary" size="small">主要</WdButton>
          <WdButton type="success" size="small">成功</WdButton>
          <WdButton type="info" size="small">信息</WdButton>
          <WdButton type="warning" size="small">警告</WdButton>
          <WdButton type="danger" size="small">危险</WdButton>
        </view>
      </WdCell>
      <WdCell title-width="0" center>
        <view class="flex flex-wrap gap-2">
          <WdButton type="primary" variant="plain" size="small">主要</WdButton>
          <WdButton type="success" variant="plain" size="small">成功</WdButton>
          <WdButton type="info" variant="plain" size="small">信息</WdButton>
          <WdButton type="warning" variant="plain" size="small">警告</WdButton>
          <WdButton type="danger" variant="plain" size="small">危险</WdButton>
        </view>
      </WdCell>
      <WdCell title-width="0" center>
        <view class="flex flex-wrap gap-2">
          <WdButton type="primary" round size="small">圆角</WdButton>
          <WdButton type="success" round size="small">圆角</WdButton>
          <WdButton type="info" round size="small">圆角</WdButton>
        </view>
      </WdCell>
      <WdCell title-width="0" center>
        <view class="flex flex-wrap gap-2">
          <WdButton type="primary" loading size="small" />
          <WdButton type="primary" disabled size="small">禁用</WdButton>
          <WdButton type="danger" size="small">
            <WdIcon name="delete" size="14px" />
          </WdButton>
        </view>
      </WdCell>
    </WdCellGroup>

    <!-- 标签 -->
    <WdCellGroup title="标签 Tag" border>
      <WdCell title-width="0" center>
        <view class="flex flex-wrap gap-2">
          <WdTag type="primary">primary</WdTag>
          <WdTag type="success">success</WdTag>
          <WdTag type="default">info</WdTag>
          <WdTag type="warning">warning</WdTag>
          <WdTag type="danger">danger</WdTag>
          <WdTag>default</WdTag>
        </view>
      </WdCell>
      <WdCell title-width="0" center>
        <view class="flex flex-wrap gap-2">
          <WdTag type="primary" mark>mark</WdTag>
          <WdTag type="success" mark>mark</WdTag>
          <WdTag type="default" mark>mark</WdTag>
        </view>
      </WdCell>
      <WdCell title-width="0" center>
        <view class="flex flex-wrap gap-2">
          <WdTag type="primary" round>round</WdTag>
          <WdTag type="success" round>round</WdTag>
          <WdTag type="danger" round>round</WdTag>
        </view>
      </WdCell>
    </WdCellGroup>

    <!-- 输入框 -->
    <WdCellGroup title="输入框 Input" border>
      <WdCell title-width="0">
        <WdInput v-model="inputValue" placeholder="请输入内容" clearable prefix-icon="search" suffix-icon="scan" show-word-limit :maxlength="20" />
      </WdCell>
      <WdCell title-width="0">
        <WdInput v-model="inputValue" placeholder="密码输入框" show-password clearable prefix-icon="lock" />
      </WdCell>
      <WdCell title-width="0">
        <WdInput v-model="inputValue" placeholder="禁用状态" disabled />
      </WdCell>
    </WdCellGroup>

    <!-- 开关与复选框 -->
    <WdCellGroup title="开关与选择" border>
      <WdCell title="开关 Switch" center>
        <WdSwitch v-model="switchValue" />
      </WdCell>
      <WdCell title="复选框 Checkbox" center>
        <WdCheckbox v-model="checkboxValue">同意协议</WdCheckbox>
      </WdCell>
      <WdCell title-width="0">
        <WdRadioGroup v-model="radioValue" shape="button">
          <WdRadio value="1">选项一</WdRadio>
          <WdRadio value="2">选项二</WdRadio>
          <WdRadio value="3">选项三</WdRadio>
        </WdRadioGroup>
      </WdCell>
    </WdCellGroup>

    <!-- 加载 -->
    <WdCellGroup title="加载 Loading" border>
      <WdCell title-width="0" center>
        <view class="flex flex-wrap gap-4 items-center">
          <WdLoading />
          <WdLoading type="circular" />
          <WdLoading color="#e91e63" />
        </view>
      </WdCell>
    </WdCellGroup>

    <!-- 进度条 -->
    <WdCellGroup title="进度条 Progress" border>
      <WdCell title-width="0">
        <view class="flex flex-col gap-3">
          <WdProgress :percentage="30" />
          <WdProgress :percentage="60" status="success" />
          <WdProgress :percentage="90" status="danger" />
        </view>
      </WdCell>
    </WdCellGroup>
  </view>
</template>

整个过程中,你不需要手写任何样式覆盖逻辑,不需要在页面间传递事件,不需要处理初始化时序。

当用户切换 “主题预设” 时,这些 UI 组件的样式会在全局生效。

4. 不止是主题切换

全局主题切换是 Oiyo 赋能最直观的例子,但不是唯一场景。App.vue 能渲染根部视图后,凡是需要"根部统一控制、全应用共享"的能力,都能放进去:

  • 全局 Loading/Error 状态管理
  • 统一的权限拦截布局
  • 多语言切换 Provider
  • 应用级 Toast/Notify 挂载

Oiyo 做的只是把小程序侧缺失的那层"根部视图"还给开发者。这一层补上,UniApp 的开发体验就向标准 Vue 靠拢了一大步。

回到主题切换这件事本身。不需要手写样式覆盖,不需要页面间事件传递,不需要操心初始化时序。Oiyo 在底层只做了一件事:把 App.vue 的渲染权真正交给你。

但就是这一步,把"每个页面重复定义"的痛点变成了"定义一次,全局生效"。

关于

我是 skiyee 你也可以叫我 sky

完整文档在 oiyo 官网: oiyo.js.org

组件文档在 wot 官网:wot-ui.cn

最佳实践项目仓库地址: github / gitee (need star!!!)

需要添加社群,请看 官方文档交流群


如果这篇文章对你有帮助,可以 👍点赞、💬评论、⭐收藏,让我更有动力写下一篇!