示例项目

通过实际示例学习如何使用 Root

本文档通过实际示例展示如何在不同场景下使用 Root 解决 Uniapp 开发中的问题。每个示例都包含完整的代码实现和详细说明,帮助您快速掌握 Root 的使用方法。

示例项目结构

所有示例项目都遵循以下基本结构:

example-project/
├── src/
│   ├── App.ku.vue          # 虚拟根组件
│   ├── main.ts             # 应用入口文件
│   ├── pages/              # 页面目录
│   │   └── index.vue       # 首页
│   ├── components/         # 组件目录
│   │   └── GlobalToast.vue # 全局组件示例
│   └── composables/        # 组合式 API 目录
│       └── useToast.ts    # Toast 组合式 API
├── vite.config.ts          # Vite 配置文件
└── package.json            # 项目依赖

示例一:全局共享 Toast 组件

Toast 是移动应用中常见的 UI 组件,用于显示简短的消息提示。通过 Root,我们可以轻松实现一个全局共享的 Toast 组件,在应用的任何页面中调用。

实现目标

  • 创建一个全局 Toast 组件
  • 在任何页面中显示和隐藏 Toast
  • 支持自定义 Toast 内容和样式

实现步骤

1. 创建 Toast 组件

首先,创建一个 Toast 组件:

<!-- src/components/GlobalToast.vue -->
<script setup lang="ts">
import { useToast } from '@/composables/useToast'

const { toastState, toastMessage, hideToast } = useToast()
</script>

<template>
  <transition name="toast-fade">
    <div v-if="toastState" class="toast-overlay" @click="hideToast">
      <div class="toast-container" @click.stop>
        <div class="toast-icon">ℹ️</div>
        <div class="toast-message">{{ toastMessage }}</div>
      </div>
    </div>
  </transition>
</template>

<style scoped>
.toast-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
}

.toast-container {
  background: white;
  border-radius: 8px;
  padding: 16px 24px;
  max-width: 80%;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  display: flex;
  align-items: center;
  animation: toast-slide-in 0.3s ease-out;
}

.toast-icon {
  font-size: 24px;
  margin-right: 12px;
}

.toast-message {
  font-size: 16px;
  color: #333;
  line-height: 1.4;
}

.toast-fade-enter-active,
.toast-fade-leave-active {
  transition: opacity 0.3s;
}

.toast-fade-enter-from,
.toast-fade-leave-to {
  opacity: 0;
}

@keyframes toast-slide-in {
  from {
    transform: translateY(-20px);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}
</style>

2. 实现 Toast 组合式 API

创建一个组合式 API 来管理 Toast 的状态和行为:

// src/composables/useToast.ts
import { ref } from 'vue'

const toastState = ref(false)
const toastMessage = ref('')

export function useToast() {
  function showToast(message: string, duration: number = 3000) {
    toastMessage.value = message
    toastState.value = true
    
    // 自动隐藏
    setTimeout(() => {
      hideToast()
    }, duration)
  }
  
  function hideToast() {
    toastState.value = false
  }
  
  return {
    toastState,
    toastMessage,
    showToast,
    hideToast
  }
}

3. 挂载至 App.ku.vue

在虚拟根组件中引入并使用 Toast 组件:

<!-- src/App.ku.vue -->
<script setup lang="ts">
import GlobalToast from '@/components/GlobalToast.vue'
</script>

<template>
  <!-- 页面内容将渲染在这里 -->
  <KuRootView />
  
  <!-- 全局 Toast 组件 -->
  <GlobalToast />
</template>

4. 在页面中调用 Toast

现在,您可以在任何页面中使用 Toast:

<!-- src/pages/index.vue -->
<script setup lang="ts">
import { useToast } from '@/composables/useToast'

const { showToast } = useToast()

function showSuccessToast() {
  showToast('操作成功!', 2000)
}

function showErrorToast() {
  showToast('操作失败,请重试', 3000)
}

function showCustomToast() {
  showToast('这是一条自定义时长的提示消息', 5000)
}
</script>

<template>
  <view class="container">
    <text>Toast 示例</text>
    
    <button @click="showSuccessToast">显示成功提示</button>
    <button @click="showErrorToast">显示错误提示</button>
    <button @click="showCustomToast">显示自定义时长提示</button>
  </view>
</template>

<style scoped>
.container {
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

button {
  padding: 10px 16px;
  background-color: #007aff;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
}
</style>

扩展功能

您可以根据需要扩展 Toast 组件的功能:

1. 支持不同类型的 Toast

// src/composables/useToast.ts
type ToastType = 'info' | 'success' | 'warning' | 'error'

const toastType = ref<ToastType>('info')

export function useToast() {
  function showToast(message: string, type: ToastType = 'info', duration: number = 3000) {
    toastMessage.value = message
    toastType.value = type
    toastState.value = true
    
    setTimeout(() => {
      hideToast()
    }, duration)
  }
  
  // ... 其他代码
}

2. 在组件中根据类型显示不同样式

<!-- src/components/GlobalToast.vue -->
<script setup lang="ts">
import { useToast } from '@/composables/useToast'

const { toastState, toastMessage, toastType, hideToast } = useToast()

// 根据类型获取图标
const getIcon = (type: string) => {
  switch (type) {
    case 'success': return '✅'
    case 'warning': return '⚠️'
    case 'error': return '❌'
    default: return 'ℹ️'
  }
}
</script>

<template>
  <transition name="toast-fade">
    <div v-if="toastState" class="toast-overlay" @click="hideToast">
      <div class="toast-container" :class="`toast-${toastType}`" @click.stop>
        <div class="toast-icon">{{ getIcon(toastType) }}</div>
        <div class="toast-message">{{ toastMessage }}</div>
      </div>
    </div>
  </transition>
</template>

<style scoped>
/* ... 原有样式 ... */

.toast-success .toast-container {
  border-left: 4px solid #4caf50;
}

.toast-warning .toast-container {
  border-left: 4px solid #ff9800;
}

.toast-error .toast-container {
  border-left: 4px solid #f44336;
}
</style>

示例二:全局共享 ConfigProvider

ConfigProvider 是一种常见的设计模式,用于为整个应用提供全局配置,如主题、语言、组件默认值等。通过 Root,我们可以轻松实现一个全局的 ConfigProvider。

实现目标

  • 创建一个全局 ConfigProvider 组件
  • 支持主题切换功能
  • 在任何页面中访问和修改主题配置

实现步骤

1. 定义主题类型和配置

首先,定义主题相关的类型和默认配置:

// src/types/theme.ts
export type ThemeMode = 'light' | 'dark'

export interface ThemeVars {
  primaryColor?: string
  secondaryColor?: string
  backgroundColor?: string
  textColor?: string
  borderColor?: string
  borderRadius?: string
}

export interface ThemeConfig {
  mode: ThemeMode
  vars: ThemeVars
}

2. 实现主题组合式 API

创建一个管理主题的组合式 API:

// src/composables/useTheme.ts
import { ref, computed, watch } from 'vue'
import type { ThemeMode, ThemeVars, ThemeConfig } from '@/types/theme'

// 默认主题变量
const defaultThemeVars: ThemeVars = {
  primaryColor: '#007aff',
  secondaryColor: '#5ac8fa',
  backgroundColor: '#ffffff',
  textColor: '#000000',
  borderColor: '#e5e5e5',
  borderRadius: '8px'
}

// 暗色主题变量
const darkThemeVars: ThemeVars = {
  primaryColor: '#0a84ff',
  secondaryColor: '#64d2ff',
  backgroundColor: '#000000',
  textColor: '#ffffff',
  borderColor: '#38383a',
  borderRadius: '8px'
}

const themeMode = ref<ThemeMode>('light')
const themeVars = ref<ThemeVars>({ ...defaultThemeVars })

// 计算当前主题配置
const currentTheme = computed<ThemeConfig>(() => ({
  mode: themeMode.value,
  vars: themeVars.value
}))

// 应用主题变量到 CSS 变量
function applyThemeVars(vars: ThemeVars) {
  const root = document.documentElement
  
  Object.entries(vars).forEach(([key, value]) => {
    if (value !== undefined) {
      const cssVarName = `--theme-${kebabCase(key)}`
      root.style.setProperty(cssVarName, value)
    }
  })
}

// 转换为 kebab-case
function kebabCase(str: string) {
  return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
}

// 切换主题模式
function toggleTheme(mode?: ThemeMode) {
  const newMode = mode || (themeMode.value === 'light' ? 'dark' : 'light')
  themeMode.value = newMode
  
  // 更新主题变量
  if (newMode === 'dark') {
    themeVars.value = { ...darkThemeVars, ...themeVars.value }
  } else {
    themeVars.value = { ...defaultThemeVars, ...themeVars.value }
  }
  
  // 应用主题变量
  applyThemeVars(themeVars.value)
  
  // 保存到本地存储
  localStorage.setItem('theme-mode', newMode)
}

// 更新主题变量
function updateThemeVars(vars: Partial<ThemeVars>) {
  themeVars.value = { ...themeVars.value, ...vars }
  applyThemeVars(themeVars.value)
  
  // 保存到本地存储
  localStorage.setItem('theme-vars', JSON.stringify(themeVars.value))
}

// 初始化主题
function initTheme() {
  // 从本地存储恢复主题设置
  const savedMode = localStorage.getItem('theme-mode') as ThemeMode
  const savedVars = localStorage.getItem('theme-vars')
  
  if (savedMode) {
    themeMode.value = savedMode
  }
  
  if (savedVars) {
    try {
      const parsedVars = JSON.parse(savedVars)
      themeVars.value = { ...themeVars.value, ...parsedVars }
    } catch (e) {
      console.error('Failed to parse theme vars from localStorage', e)
    }
  }
  
  // 应用主题变量
  applyThemeVars(themeVars.value)
}

// 监听主题模式变化
watch(themeMode, (newMode) => {
  if (newMode === 'dark') {
    themeVars.value = { ...darkThemeVars, ...themeVars.value }
  } else {
    themeVars.value = { ...defaultThemeVars, ...themeVars.value }
  }
  applyThemeVars(themeVars.value)
})

export function useTheme(initialVars?: ThemeVars) {
  // 初始化主题变量
  if (initialVars) {
    updateThemeVars(initialVars)
  }
  
  return {
    themeMode,
    themeVars,
    currentTheme,
    toggleTheme,
    updateThemeVars,
    initTheme
  }
}

3. 创建 ConfigProvider 组件

创建一个 ConfigProvider 组件来包裹整个应用:

<!-- src/components/ConfigProvider.vue -->
<script setup lang="ts">
import { provide, onMounted } from 'vue'
import { useTheme } from '@/composables/useTheme'
import type { ThemeConfig } from '@/types/theme'

// 初始化主题
const { currentTheme, initTheme } = useTheme()

// 提供主题配置给子组件
provide('themeConfig', currentTheme)

// 组件挂载时初始化主题
onMounted(() => {
  initTheme()
})
</script>

<template>
  <div class="config-provider" :class="`theme-${currentTheme.mode}`">
    <slot />
  </div>
</template>

<style>
/* 全局主题变量 */
:root {
  --theme-primary-color: #007aff;
  --theme-secondary-color: #5ac8fa;
  --theme-background-color: #ffffff;
  --theme-text-color: #000000;
  --theme-border-color: #e5e5e5;
  --theme-border-radius: 8px;
}

/* 暗色主题 */
.theme-dark {
  color: var(--theme-text-color);
  background-color: var(--theme-background-color);
}
</style>

4. 挂载至 App.ku.vue

在虚拟根组件中使用 ConfigProvider:

<!-- src/App.ku.vue -->
<script setup lang="ts">
import { useTheme } from '@/composables/useTheme'
import ConfigProvider from '@/components/ConfigProvider.vue'

// 初始化主题
const { themeMode, toggleTheme } = useTheme({
  primaryColor: '#07c160'
})
</script>

<template>
  <ConfigProvider>
    <div class="app-root" :class="`theme-${themeMode}`">
      <div class="app-header">
        <h1>Root 主题示例</h1>
        <button @click="toggleTheme()">
          切换主题 (当前: {{ themeMode }})
        </button>
      </div>
      
      <!-- 页面内容将渲染在这里 -->
      <KuRootView />
    </div>
  </ConfigProvider>
</template>

<style scoped>
.app-root {
  min-height: 100vh;
  transition: background-color 0.3s, color 0.3s;
}

.app-header {
  padding: 16px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1px solid var(--theme-border-color);
}

button {
  padding: 8px 16px;
  background-color: var(--theme-primary-color);
  color: white;
  border: none;
  border-radius: var(--theme-border-radius);
  cursor: pointer;
}
</style>

5. 在页面中使用主题

现在,您可以在任何页面中使用和修改主题:

<!-- src/pages/theme-demo.vue -->
<script setup lang="ts">
import { inject, ref } from 'vue'
import type { ThemeConfig } from '@/types/theme'

// 注入主题配置
const themeConfig = inject('themeConfig') as ThemeConfig

// 自定义主题变量
const customPrimaryColor = ref('#07c160')

function applyCustomColor() {
  // 通过根组件实例修改主题
  const pagesStack = getCurrentPages()
  if (pagesStack.length > 0) {
    const rootInstance = pagesStack[pagesStack.length - 1].$vm
    if (rootInstance && rootInstance.$refs.uniKuRoot) {
      rootInstance.$refs.uniKuRoot.updateThemeVars({
        primaryColor: customPrimaryColor.value
      })
    }
  }
}
</script>

<template>
  <view class="theme-demo">
    <text>当前主题: {{ themeConfig.mode }}</text>
    <text>主色调: {{ themeConfig.vars.primaryColor }}</text>
    
    <view class="color-picker">
      <text>自定义主色调:</text>
      <input v-model="customPrimaryColor" type="color" />
      <button @click="applyCustomColor">应用</button>
    </view>
    
    <view class="demo-components">
      <button class="primary-btn">主要按钮</button>
      <button class="secondary-btn">次要按钮</button>
    </view>
  </view>
</template>

<style scoped>
.theme-demo {
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.color-picker {
  display: flex;
  align-items: center;
  gap: 8px;
}

.demo-components {
  display: flex;
  gap: 12px;
}

.primary-btn {
  padding: 10px 16px;
  background-color: var(--theme-primary-color);
  color: white;
  border: none;
  border-radius: var(--theme-border-radius);
}

.secondary-btn {
  padding: 10px 16px;
  background-color: var(--theme-secondary-color);
  color: white;
  border: none;
  border-radius: var(--theme-border-radius);
}
</style>

示例三:使用 Wot 组件库的 Toast 和 Notify

Wot Design Uni 是一个流行的 Uniapp 组件库,它提供了丰富的 UI 组件。通过 Root,我们可以更好地集成和使用这些组件,特别是那些需要全局挂载的组件。

实现目标

  • 集成 Wot Design Uni 组件库
  • 全局挂载 Toast 和 Notify 组件
  • 在页面中方便地调用这些组件

实现步骤

1. 安装 Wot Design Uni

首先,安装 Wot Design Uni:

# 使用 npm
npm install wot-design-uni

# 使用 yarn
yarn add wot-design-uni

# 使用 pnpm
pnpm add wot-design-uni

2. 配置 Wot Design Uni

main.ts 中配置 Wot Design Uni:

// src/main.ts
import { createSSRApp } from 'vue'
import App from './App.vue'
import WotDesignUni from 'wot-design-uni'

export function createApp() {
  const app = createSSRApp(App)
  app.use(WotDesignUni)
  return {
    app
  }
}

3. 挂载 Wot 组件至 App.ku.vue

在虚拟根组件中挂载 Wot 的 Toast 和 Notify 组件:

<!-- src/App.ku.vue -->
<script setup lang="ts">
// 可以在这里添加一些全局逻辑
</script>

<template>
  <!-- 页面内容将渲染在这里 -->
  <KuRootView />
  
  <!-- 全局挂载 Wot 组件 -->
  <wd-toast />
  <wd-notify />
</template>

4. 在页面中使用 Wot 组件

现在,您可以在任何页面中使用 Wot 的 Toast 和 Notify 组件:

<!-- src/pages/wot-demo.vue -->
<script setup lang="ts">
import { useToast, useNotify } from 'wot-design-uni'

const toast = useToast()
const notify = useNotify()

function showToast() {
  toast.show('这是一条 Toast 消息')
}

function showSuccessToast() {
  toast.success('操作成功')
}

function showErrorToast() {
  toast.error('操作失败')
}

function showNotify() {
  notify({
    message: '这是一条通知消息',
    type: 'success',
    duration: 3000
  })
}

function showWarningNotify() {
  notify({
    message: '这是一条警告通知',
    type: 'warning'
  })
}

function showErrorNotify() {
  notify({
    message: '这是一条错误通知',
    type: 'error'
  })
}
</script>

<template>
  <view class="wot-demo">
    <text>Wot Design Uni 组件示例</text>
    
    <view class="section">
      <text>Toast 示例</text>
      <button @click="showToast">显示 Toast</button>
      <button @click="showSuccessToast">显示成功 Toast</button>
      <button @click="showErrorToast">显示错误 Toast</button>
    </view>
    
    <view class="section">
      <text>Notify 示例</text>
      <button @click="showNotify">显示通知</button>
      <button @click="showWarningNotify">显示警告通知</button>
      <button @click="showErrorNotify">显示错误通知</button>
    </view>
  </view>
</template>

<style scoped>
.wot-demo {
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.section {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

text {
  font-size: 16px;
  font-weight: bold;
}

button {
  padding: 10px 16px;
  background-color: #1989fa;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 14px;
}
</style>

高级用法

1. 封装全局方法

您可以在根组件中封装一些全局方法,方便在页面中调用:

<!-- src/App.ku.vue -->
<script setup lang="ts">
import { ref } from 'vue'

// 封装全局方法
function showGlobalToast(message: string, type = 'info') {
  // 这里可以使用任何 Toast 实现
  console.log(`Global Toast: ${message} (${type})`)
  
  // 如果使用 Wot 的 Toast
  // const toast = useToast()
  // toast[type](message)
}

function showGlobalNotify(message: string, type = 'info') {
  // 这里可以使用任何 Notify 实现
  console.log(`Global Notify: ${message} (${type})`)
  
  // 如果使用 Wot 的 Notify
  // const notify = useNotify()
  // notify({ message, type })
}

// 暴露给页面使用
defineExpose({
  showGlobalToast,
  showGlobalNotify
})
</script>

<template>
  <KuRootView />
  <wd-toast />
  <wd-notify />
</template>

2. 在页面中调用全局方法

<!-- src/pages/global-methods.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'

const rootInstance = ref()

onMounted(() => {
  // 获取根组件实例
  const pagesStack = getCurrentPages()
  if (pagesStack.length > 0) {
    rootInstance.value = pagesStack[pagesStack.length - 1].$vm.$refs.uniKuRoot
  }
})

function callGlobalToast() {
  if (rootInstance.value) {
    rootInstance.value.showGlobalToast('这是全局 Toast', 'success')
  }
}

function callGlobalNotify() {
  if (rootInstance.value) {
    rootInstance.value.showGlobalNotify('这是全局通知', 'warning')
  }
}
</script>

<template root="rootInstance">
  <view class="global-methods">
    <text>全局方法调用示例</text>
    <button @click="callGlobalToast">调用全局 Toast</button>
    <button @click="callGlobalNotify">调用全局 Notify</button>
  </view>
</template>

<style scoped>
.global-methods {
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

button {
  padding: 10px 16px;
  background-color: #07c160;
  color: white;
  border: none;
  border-radius: 4px;
}
</style>

示例总结

通过以上三个示例,我们展示了 Root 在不同场景下的应用:

  1. 全局共享 Toast 组件:展示了如何创建和使用全局共享的 UI 组件,解决了 Uniapp 中无法在根级别挂载组件的问题。
  2. 全局共享 ConfigProvider:展示了如何实现全局主题管理,包括主题切换、主题变量自定义等功能,为应用提供了一致的外观和体验。
  3. 使用 Wot 组件库的 Toast 和 Notify:展示了如何与第三方组件库集成,充分利用 Root 的虚拟根组件特性,更好地使用这些组件。

这些示例涵盖了 Root 的主要功能和使用场景,您可以根据实际项目需求进行调整和扩展。通过合理使用 Root,您可以构建更加灵活、可维护的 Uniapp 应用。


更多示例和最佳实践,请参考 GitHub 仓库 中的示例项目。