Skip to content

业务组件

介绍

业务组件是针对 RuoYi-Plus-UniApp 项目业务场景封装的专用组件,深度集成认证系统、支付系统、状态管理等核心功能。

核心特性:

  • 认证授权 - 完整的用户认证流程,支持多平台登录、用户信息授权
  • 支付集成 - 多平台支付能力,支持微信支付、支付宝支付、余额支付
  • 状态管理 - 基于 Pinia 的用户状态管理
  • 多平台适配 - 自动识别运行环境,适配多平台

组件列表

组件说明位置
AuthModal用户授权弹窗组件components/auth/AuthModal.vue
Home首页业务组件components/tabbar/Home.vue
Menu分类菜单组件components/tabbar/Menu.vue
My个人中心组件components/tabbar/My.vue

AuthModal 授权弹窗

用户授权弹窗组件,用于收集用户信息(头像、昵称),并可选择性绑定手机号。

基本用法

vue
<template>
  <view>
    <AuthModal />
    <wd-button @click="userStore.authModalVisible = true">完善信息</wd-button>
  </view>
</template>

<script lang="ts" setup>
const userStore = useUserStore()
</script>

头像选择与上传

vue
<template>
  <wd-button
    type="icon"
    :icon="avatarPreviewUrl || 'camera'"
    open-type="chooseAvatar"
    @chooseavatar="chooseavatar"
    @click="manualChooseAvatar"
  />
</template>

<script lang="ts" setup>
import { useUpload, useToast } from '@/wd'

const toast = useToast()
const upload = useUpload()
const avatarPreviewUrl = ref('')
const form = ref({ avatar: '', nickName: '' })

// 微信小程序头像选择
const chooseavatar = (detail) => {
  // #ifdef MP-WEIXIN
  toast.loading('正在上传头像...')
  upload.fastUpload(detail.avatarUrl, {
    onSuccess(res) {
      toast.close()
      avatarPreviewUrl.value = res.url
      form.value.avatar = res.originalUrl!
    },
    onError() { toast.close(); toast.error('上传失败') },
  })
  // #endif
}

// 其他平台手动选择
const manualChooseAvatar = async () => {
  // #ifndef MP-WEIXIN
  const res = await upload.chooseFile({ accept: 'image', maxCount: 1 })
  toast.loading('正在上传头像...')
  upload.fastUpload(res[0].path, {
    onSuccess(uploadRes) {
      toast.close()
      avatarPreviewUrl.value = uploadRes.url
      form.value.avatar = uploadRes.originalUrl!
    },
  })
  // #endif
}
</script>

完整授权流程

typescript
const completeAuth = async () => {
  if (!form.value.avatar) { toast.warning('请选择头像'); return }
  if (!form.value.nickName) { toast.warning('请设置昵称'); return }

  const [err] = await updateUserProfile({
    avatar: form.value.avatar,
    nickName: form.value.nickName,
  })

  if (!err) {
    await userStore.fetchUserInfo()
    userStore.authModalVisible = false
    toast.success('授权成功!')
  }
}

Home 首页组件

首页业务组件,集成轮播图、金刚区导航、商品列表分页、支付功能等。

基本用法

vue
<template>
  <view class="min-h-[100vh]">
    <wd-navbar title="首页" />
    <wd-swiper :list="swiperList" custom-class="m-2" />

    <wd-row custom-class="p-2 bg-white mx-4 rounded-xl" :gutter="16">
      <wd-col v-for="(item, index) in menuList" :key="index" :span="6">
        <view class="flex flex-col items-center py-2" @click="handleMenuClick(item)">
          <wd-icon :name="item.icon" size="60" :color="item.color" />
          <wd-text custom-class="mt-2" :text="item.title" />
        </view>
      </wd-col>
    </wd-row>

    <wd-paging
      ref="paging"
      :fetch="pageGoods"
      :params="queryParams"
      :tabs="tabsConfig"
    >
      <template #item="{ item }">
        <wd-card custom-class="w-694rpx">
          <template #title>
            <view class="flex items-center">
              <wd-img :src="item.img" width="120" height="120" radius="8" />
              <wd-text custom-class="ml-1" :text="item.name" />
            </view>
          </template>
          <template #footer>
            <view class="flex justify-between">
              <wd-text :text="item.price" size="32" type="error" bold />
              <wd-button type="primary" size="small" @click="handleGoodsPay(item)">
                立即购买
              </wd-button>
            </view>
          </template>
        </wd-card>
      </template>
    </wd-paging>
  </view>
</template>

数据配置

typescript
const tabsConfig = ref([
  { name: 'hot', title: '热销', data: { category: 'hot' } },
  { name: 'new', title: '新品', data: { category: 'new' } },
])

const menuList = ref([
  { title: '外卖', icon: 'goods', color: '#ff6b6b' },
  { title: '超市', icon: 'cart', color: '#4ecdc4' },
  // ...更多菜单项
])

const queryParams = ref<GoodsQuery>({
  pageNum: 1,
  pageSize: 10,
  orderByColumn: 'createTime',
  isAsc: 'desc',
})

商品支付功能

typescript
import { usePayment } from '@/composables/usePayment'
import { PaymentMethod } from '@/api/common/mall/order/orderTypes'

const { createOrderAndPay, pollOrderStatus } = usePayment()

const handleGoodsPay = async (goods: GoodsVo) => {
  const orderData: CreateOrderBo = {
    goodsId: goods.id,
    goodsName: goods.name,
    quantity: 1,
    price: '0.01',
  }

  const [payErr, payResult] = await createOrderAndPay({
    orderData,
    paymentMethod: PaymentMethod.WECHAT,
  })

  if (!payErr) {
    const [pollErr] = await pollOrderStatus(payResult.orderData.orderNo, 5)
    if (!pollErr) toast.success('支付成功')
    else toast.error('支付确认超时')
  }
}

实现侧边栏分类导航与内容滚动联动效果。

基本用法

vue
<template>
  <view>
    <wd-navbar title="点餐" />
    <view class="wraper">
      <wd-sidebar v-model="active" @change="handleChange">
        <wd-sidebar-item
          v-for="(item, index) in categories"
          :key="index"
          :value="index"
          :label="item.label"
        />
      </wd-sidebar>

      <scroll-view
        class="content"
        scroll-y
        :scroll-with-animation="scrollWithAnimation"
        :scroll-top="scrollTop"
        @scroll="onScroll"
      >
        <view v-for="(item, index) in categories" :key="index" class="category">
          <wd-cell-group :title="item.title" border>
            <wd-cell
              v-for="(cell, cellIndex) in item.items"
              :key="cellIndex"
              :title="cell.title"
              :label="cell.label"
            />
          </wd-cell-group>
        </view>
      </scroll-view>
    </view>
  </view>
</template>

滚动联动逻辑

typescript
const active = ref(0)
const scrollTop = ref(0)
const itemScrollTop = ref<number[]>([])
const isScrolling = ref(false)

onMounted(() => {
  CommonUtil.getRect('.category', true, proxy).then((rects) => {
    if (CommonUtil.isArray(rects)) {
      itemScrollTop.value = rects.map((item) => item.top || 0)
      scrollTop.value = rects[active.value].top || 0
    }
  })
})

function handleChange({ value }) {
  if (isScrolling.value) return
  active.value = value
  isScrolling.value = true
  scrollTop.value = itemScrollTop.value[value]
  setTimeout(() => { isScrolling.value = false }, 300)
}

function onScroll(e) {
  if (isScrolling.value) return
  const { scrollTop: currentScrollTop } = e.detail
  const index = itemScrollTop.value.findIndex(
    (top) => top > currentScrollTop && top - currentScrollTop <= 50
  )
  if (index > -1 && active.value !== index) active.value = index
}

My 个人中心组件

个人中心页面,包含用户信息展示、统计数据、订单管理、快捷功能等。

基本用法

vue
<template>
  <view class="min-h-[100vh] bg-#FFFCF5 pb-10">
    <wd-navbar :bg-color="`rgba(255,252,245,${scrollTop / 60})`" title="我的" />

    <view class="relative pt-10">
      <view class="flex flex-col items-center justify-center">
        <wd-icon
          v-if="!userStore.userInfo?.avatar"
          custom-class="bg-#f8f6f8 rounded-full p-6"
          name="user"
          size="80"
          @click="handleUserInfo"
        />
        <wd-img v-else :src="userStore.userInfo?.avatar" width="128" height="128" round @click="handleUserInfo" />
        <wd-text size="36" :text="userStore.userInfo?.nickName || '昵称'" @click="handleUserInfo" />
      </view>

      <wd-row custom-class="mt-6 bg-#ffffffcc mx-5! rounded-lg py-2" :gutter="12">
        <wd-col v-for="(stat, index) in statsData" :key="index" :span="8">
          <view class="text-center">
            <wd-text bold block size="34" :text="stat.value" />
            <wd-text block :text="stat.label" size="24" />
          </view>
        </wd-col>
      </wd-row>
    </view>

    <wd-cell-group custom-class="mt-2 mx-3" title="我的订单">
      <wd-grid :items="orderTypes" clickable @item-click="handleOrderClick" />
    </wd-cell-group>

    <wd-button v-if="auth.isLoggedIn.value" block custom-class="mx-10! mt-4" @click="handleLogout">
      退出登录
    </wd-button>
  </view>
</template>

数据与事件

typescript
const statsData = ref([
  { label: '优惠券', value: '3', type: 'coupon' },
  { label: '积分', value: '1285', type: 'points' },
  { label: '余额', value: '268', type: 'balance' },
])

const orderTypes = ref<GridItem[]>([
  { text: '待付款', icon: 'wallet', iconColor: '#666666' },
  { text: '待收货', icon: 'calendar', iconColor: '#666666' },
  { text: '待评价', icon: 'star', iconColor: '#666666' },
  { text: '退款/售后', icon: 'service', iconColor: '#666666' },
])

const handleUserInfo = () => {
  if (auth.isLoggedIn.value) userStore.authModalVisible = true
  else uni.navigateTo({ url: '/pages/auth/login' })
}

const handleLogout = async () => {
  const result = await confirm({ title: '确认退出', msg: '您确定要退出登录吗?' })
  if (result.action === 'confirm') {
    toast.loading('退出中...')
    const [err] = await userStore.logoutUser()
    if (!err) { toast.close(); toast.success('退出成功') }
  }
}

useAuth 认证组合式函数

提供用户认证与权限检查功能。

基本用法

typescript
import { useAuth } from '@/composables/useAuth'

const {
  isLoggedIn,      // 登录状态
  isSuperAdmin,    // 超级管理员判断
  hasPermission,   // 权限检查
  hasRole,         // 角色检查
} = useAuth()

// 单个权限检查
if (hasPermission('system:user:add')) { /* 可以添加用户 */ }

// 多个权限检查(OR 逻辑)
if (hasPermission(['system:user:add', 'system:user:update'])) { /* 有任一权限 */ }

// 角色检查
if (hasRole('admin')) { /* 是管理员 */ }

API 说明

方法/属性说明返回值
isLoggedIn登录状态ComputedRef<boolean>
isSuperAdmin超级管理员判断boolean
isTenantAdmin租户管理员判断boolean
hasPermission权限检查boolean
hasRole角色检查boolean
hasAllPermissions检查所有权限boolean
canAccessRoute路由访问检查boolean
filterAuthorizedRoutes过滤授权路由any[]

usePayment 支付组合式函数

多平台支付组合式函数,支持微信支付、支付宝支付、余额支付。

基本用法

typescript
import { usePayment } from '@/composables/usePayment'
import { PaymentMethod } from '@/api/common/mall/order/orderTypes'

const {
  loading,
  availableMethods,
  createOrderAndPay,
  payOrder,
  pollOrderStatus,
  getPlatformInfo,
} = usePayment()

创建订单并支付

typescript
const handlePay = async () => {
  const orderData: CreateOrderBo = {
    goodsId: 'goods-001',
    goodsName: '测试商品',
    quantity: 1,
    price: '99.00',
  }

  const [err, result] = await createOrderAndPay({
    orderData,
    paymentMethod: PaymentMethod.WECHAT,
  })

  if (!err && result) {
    const [pollErr] = await pollOrderStatus(result.orderData.orderNo, 5)
    if (!pollErr) console.log('支付成功')
  }
}

API 说明

方法/属性说明返回值
loading支付加载状态Readonly<Ref<boolean>>
availableMethods可用支付方式Readonly<Ref<PaymentMethod[]>>
createOrderAndPay创建订单并支付Promise<[Error | null, Result]>
payOrder支付已有订单Promise<[Error | null, Result]>
pollOrderStatus轮询订单状态Promise<[Error, Result]>
getPlatformInfo获取平台信息PlatformInfo

useUserStore 用户状态管理

基于 Pinia 的用户认证与权限管理模块。

基本用法

typescript
import { useUserStore } from '@/stores/modules/user'

const userStore = useUserStore()
const nickname = computed(() => userStore.userInfo?.nickName || '')
const avatar = computed(() => userStore.userInfo?.avatar || '')

登录方法

typescript
// 密码登录
const [err] = await userStore.loginWithPassword({
  userName: 'admin',
  password: '123456',
  code: '1234',
  uuid: 'uuid123',
})

// 小程序一键登录
const [err] = await userStore.loginWithMiniapp()

// 微信公众号登录
const authUrl = userStore.getWechatH5AuthUrl('https://your-domain.com/callback')
const [err] = await userStore.loginWithMp({ code, state })

// 获取用户信息
await userStore.fetchUserInfo()

// 退出登录
await userStore.logoutUser()

API 说明

方法/属性说明返回值
token用户令牌Ref<string>
isLoggedIn登录状态ComputedRef<boolean>
userInfo用户信息Ref<SysUserVo | null>
roles用户角色Ref<string[]>
permissions用户权限Ref<string[]>
loginWithPassword密码登录Result<AuthTokenVo>
loginWithMiniapp小程序登录Result<AuthTokenVo>
loginWithMp公众号登录Result<AuthTokenVo>
fetchUserInfo获取用户信息Result<UserInfoVo>
logoutUser用户注销Result<void>
authModalVisible授权弹窗状态Ref<boolean>
navigateWithUserCheck带用户检查的跳转void

平台登录支持

平台支持的登录方式
微信小程序miniapp, password, sms
微信公众号mp, password
抖音/支付宝小程序miniapp, password
H5password, sms
APPpassword

最佳实践

1. 使用组合式函数封装业务逻辑

typescript
export const useOrder = () => {
  const orderList = ref([])
  const loading = ref(false)

  const fetchOrders = async () => {
    loading.value = true
    const [err, data] = await getOrderList()
    if (!err) orderList.value = data
    loading.value = false
  }

  return { orderList, loading, fetchOrders }
}

2. 错误处理统一封装

typescript
const [err, data] = await someApi()
if (err) {
  toast.error(err.message || '操作失败')
  return
}

3. 平台适配使用条件编译

typescript
// #ifdef MP-WEIXIN
const res = await uni.login()
// #endif

// #ifndef MP-WEIXIN
const res = await customLogin()
// #endif

常见问题

1. 授权弹窗不显示

确保 AuthModal 组件已引入,并设置 userStore.authModalVisible = true

vue
<template>
  <AuthModal />
  <wd-button @click="userStore.authModalVisible = true">完善信息</wd-button>
</template>

2. 支付在某些平台不可用

检查平台支持的支付方式:

typescript
const { availableMethods, getPlatformInfo } = usePayment()
const platformInfo = getPlatformInfo()
console.log('支持微信支付:', platformInfo.supportsWechatPay)

3. 侧边栏与内容滚动不同步

使用 isScrolling 标志防止重复触发:

typescript
const isScrolling = ref(false)

function handleChange({ value }) {
  if (isScrolling.value) return
  isScrolling.value = true
  scrollTop.value = targetPosition
  setTimeout(() => { isScrolling.value = false }, 300)
}

类型定义

用户相关类型

typescript
interface SysUserVo {
  userId: string
  userName: string
  nickName: string
  avatar: string
  phone: string
  tenantId: string
}

interface UserInfoVo {
  user: SysUserVo
  roles: string[]
  permissions: string[]
}

interface AuthTokenVo {
  access_token: string
  expires_in: number
}

type AuthType = 'password' | 'sms' | 'miniapp' | 'mp'
type PlatformType = 'mp-weixin' | 'mp-official-account' | 'mp-toutiao' | 'mp-alipay' | 'h5' | 'app'

支付相关类型

typescript
enum PaymentMethod {
  WECHAT = 'WECHAT',
  ALIPAY = 'ALIPAY',
  BALANCE = 'BALANCE',
}

enum TradeType {
  JSAPI = 'JSAPI',
  APP = 'APP',
  H5 = 'H5',
  NATIVE = 'NATIVE',
}

interface CreateOrderBo {
  goodsId: string
  goodsName: string
  quantity: number
  price: string
}

interface PaymentRequest {
  orderNo: string
  paymentMethod: PaymentMethod
  tradeType?: TradeType
  appId?: string
  openId?: string
}

interface GridItem {
  text: string
  icon: string
  iconColor?: string
  badge?: number | string
  dot?: boolean
  url?: string
}