Skip to content
CatchAdmin 插件市场也正式上线啦!!! GO ! 还有 CatchAdmin 正在参加 Gitee 2025 最受欢迎的开源软件投票活动 ⭐请给我投一票吧!

Vue 单页组件开发

🎨 本章介绍如何在插件中开发 Vue 单页组件,以及远程组件可以使用的框架模块。

概述

CatchAdmin 插件系统支持远程加载 Vue 单文件组件(SFC),让你可以在插件中编写独立的 Vue 页面,并且能够复用主应用的 stores、composables 和 UI 组件。

💡 核心特性

  • 按需加载:组件只在访问时才会加载,不影响主应用性能
  • 模块共享:可直接使用主应用的 Pinia stores、composables 和 UI 组件
  • 熟悉的语法:使用标准的 @/ 路径别名,与主应用开发体验一致

⚠️ 重要限制

远程 SFC 组件是在运行时通过 vue3-sfc-loader 加载的,与主应用的编译时处理不同,因此存在以下限制:

必须了解的限制

1. 必须显式导入组件

主应用使用 unplugin-vue-components 实现组件自动导入,但远程组件不经过 Vite 编译,所以:

vue
<!-- ❌ 错误:自动导入在远程组件中不生效 -->
<template>
  <catch-table />
  <el-button />
</template>

<!-- ✅ 正确:必须显式导入所有使用的组件 -->
<script setup>
import CatchTable from '@/components/catchTable/index.vue'
import { ElButton } from 'element-plus'
</script>

<template>
  <CatchTable />
  <ElButton />
</template>

2. 不支持 TypeScript 类型检查

远程组件在运行时编译,不会进行 TypeScript 类型检查。建议:

  • 在开发时使用 // @ts-nocheck 注释
  • 或确保代码逻辑正确,类型问题不会在运行时报错

3. 不支持 Vite 特性

以下 Vite 特性在远程组件中不可用

  • import.meta.env 环境变量
  • import.meta.glob 动态导入
  • CSS 预处理器(如 SCSS、Less)的高级特性
  • 自动 CSS 注入

4. 调试相对困难

  • 运行时错误堆栈可能不够清晰
  • 无法使用 Vue DevTools 的完整功能
  • 建议使用 console.log 进行调试

适用场景

适合不适合
插件的后台管理页面核心业务逻辑
CRUD 操作界面高性能要求的页面
配置管理页面复杂的交互动画
简单的数据展示需要类型安全的场景

快速开始

创建 Vue 组件

在插件的 resource/view 目录下创建 Vue 组件:

packages/your-plugin/
├─resource/
│  └─view/
│     └─user/
│        ├─index.vue      # 列表页
│        ├─create.vue     # 创建/编辑表单
│        └─components/    # 子组件目录
│           └─department.vue

编写组件代码

vue
<template>
  <div>
    <CatchTable
      :columns="columns"
      api="users"
      permission="user.user"
    >
      <template #dialog="row">
        <Create :primary="row?.id" api="users" />
      </template>
    </CatchTable>
  </div>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
import Create from './create.vue'
import CatchTable from '@/components/catchTable/index.vue'
import { useUserStore } from '@/stores/modules/user'
import { isUndefined } from '@/support/helper'

const userStore = useUserStore()
const columns = [
  { type: 'selection' },
  { label: '用户名', prop: 'username' },
  { label: '邮箱', prop: 'email' },
  { type: 'operate', label: '操作', width: 200 }
]
</script>

可用模块一览

核心库(预加载)

这些模块在应用启动时就已加载,可直接使用:

模块导入方式说明
Vueimport { ref, computed } from 'vue'Vue 3 核心
Vue Routerimport { useRouter } from 'vue-router'路由
Piniaimport { storeToRefs } from 'pinia'状态管理
Element Plusimport { ElMessage } from 'element-plus'UI 组件库
Element Plus Iconsimport { Edit } from '@element-plus/icons-vue'图标
Heroiconsimport { UserIcon } from '@heroicons/vue/24/outline'图标

Stores(状态管理)

模块导入方式说明
App Storeimport { useAppStore } from '@/stores/modules/app'应用配置
User Storeimport { useUserStore } from '@/stores/modules/user'用户信息
Permissions Storeimport { usePermissionsStore } from '@/stores/modules/user/permissions'权限管理
Tabs Storeimport { useNavTabStore } from '@/stores/modules/tabs'标签页

示例:

vue
<script setup>
import { useUserStore } from '@/stores/modules/user'

const userStore = useUserStore()
const username = userStore.getUsername
const roles = userStore.getRoles
</script>

Composables(组合式函数)

CURD 操作

模块导入方式说明
useCreateimport { useCreate } from '@/composables/curd/useCreate'创建操作
useShowimport { useShow } from '@/composables/curd/useShow'查看详情
useDestroyimport { useDestroy } from '@/composables/curd/useDestroy'删除操作
useEnabledimport { useEnabled } from '@/composables/curd/useEnabled'启用/禁用
useGetListimport { useGetList } from '@/composables/curd/useGetList'获取列表
useOpenimport { useOpen } from '@/composables/curd/useOpen'打开弹窗
useRestoreimport { useRestore } from '@/composables/curd/useRestore'恢复删除
useFormSubmitimport { useFormSubmit } from '@/composables/curd/useFormSubmit'表单提交
useExcelDownloadimport { useExcelDownload } from '@/composables/curd/useExcelDownload'Excel 导出

示例:

vue
<script setup>
import { useCreate } from '@/composables/curd/useCreate'
import { useShow } from '@/composables/curd/useShow'

const props = defineProps<{ primary?: number; api: string }>()
const { formData, loading, beforeCreate } = useCreate(props.api)
const { getDetail } = useShow(props.api)

if (props.primary) {
  getDetail(props.primary).then(res => {
    Object.assign(formData, res)
  })
}
</script>

其他 Composables

模块导入方式说明
useSseimport { useSse } from '@/composables/useSse'SSE 实时通信
useUploadimport { useUpload } from '@/composables/useUpload'文件上传
useChunkUploadimport { useChunkUpload } from '@/composables/useChunkUpload'分片上传
useDynamicimport { useDynamic } from '@/composables/useDynamic'动态组件
useGetRemoteTableDataimport { useGetRemoteTableData } from '@/composables/useGetRemoteTableData'远程表格数据

Support 工具模块

模块导入方式说明
httpimport http from '@/support/http'HTTP 请求
helperimport { isUndefined, isEmpty } from '@/support/helper'辅助函数
messageimport Message from '@/support/message'消息提示
cacheimport Cache from '@/support/cache'缓存管理
requestimport request from '@/support/request'原始请求

示例:

vue
<script setup>
import http from '@/support/http'
import Message from '@/support/message'
import { isUndefined } from '@/support/helper'

const fetchData = async () => {
  const res = await http.get('/api/users')
  if (res.code === 200) {
    Message.success('获取成功')
  }
}
</script>

UI 组件

所有 @/components 目录下的组件都可以导入使用:

组件导入方式说明
CatchTableimport CatchTable from '@/components/catchTable/index.vue'数据表格
CatchFormimport CatchForm from '@/components/catchForm/index.vue'动态表单
Cavatarimport Cavatar from '@/components/admin/avatar/cavatar.vue'头像组件
Dialogimport Dialog from '@/components/admin/dialog/index.vue'弹窗组件
Uploadimport Upload from '@/components/admin/upload/index.vue'上传组件
Iconimport Icon from '@/components/icon/index.vue'图标组件
Cascaderimport Cascader from '@/components/admin/cascader/index.vue'级联选择
Selectimport Select from '@/components/admin/select/index.vue'下拉选择
Areaimport Area from '@/components/admin/area/index.vue'地区选择
Editorimport Editor from '@/components/editor/index.vue'富文本编辑器
Codeimport Code from '@/components/code/index.vue'代码编辑器

⚠️ 注意

远程组件中使用的所有组件都必须显式导入,不能依赖主应用的自动导入功能。

第三方库(懒加载)

模块导入方式说明
VueUseimport { useMouse } from '@vueuse/core'Vue 工具集
Echartsimport * as echarts from 'echarts'图表库
Vue Echartsimport VChart from 'vue-echarts'Vue 图表组件
Momentimport moment from 'moment'日期处理
Vue I18nimport { useI18n } from 'vue-i18n'国际化

路径格式说明

支持多种路径格式,以下写法等效:

javascript
// 以下导入方式都是有效的
import { useUserStore } from '@/stores/modules/user'
import { useUserStore } from '@/stores/modules/user/index'
import { useUserStore } from '@/stores/modules/user/index.ts'

import { useCreate } from '@/composables/curd/useCreate'
import { useCreate } from '@/composables/curd/useCreate.ts'

import CatchTable from '@/components/catchTable/index.vue'
import CatchTable from '@/components/catchTable/index'

不支持的特性

在开发远程组件前,请了解以下不支持的特性:

❌ 不支持的语法和特性

特性说明替代方案
<script setup lang="tsx">TSX 语法不支持使用模板语法
import.meta.env环境变量不可用通过 API 获取配置
import.meta.glob动态导入不可用手动列出导入
defineOptions()宏不支持使用普通 <script>
defineSlots()宏不支持使用 $slots
await 顶层顶层 await 不支持使用 onMounted

❌ 不支持的 CSS 特性

特性说明替代方案
SCSS/Sass预处理器不支持使用原生 CSS
Less预处理器不支持使用原生 CSS
CSS Modules<style module> 不支持使用 scoped 或 Tailwind
@import 语句CSS 导入不支持使用 Tailwind 或行内样式
CSS 变量定义:root { --var } 不生效使用主应用已定义的变量

❌ 不支持的导入方式

javascript
// ❌ 动态导入路径
const module = await import(`@/components/${name}`)

// ❌ 别名以外的路径
import xxx from 'src/xxx'
import xxx from '~/xxx'

// ❌ 直接导入 node_modules(未注册的)
import lodash from 'lodash'

// ✅ 正确:使用已注册的模块
import http from '@/support/http'
import { ref } from 'vue'

❌ 不支持的 Vue 特性

特性说明
自定义指令v-focus 等自定义指令不生效
全局 mixin主应用的 mixin 不可用
全局组件必须显式导入,不能使用全局注册的组件
异步组件defineAsyncComponent 可能不工作

⚠️ 受限的特性

特性限制说明
TypeScript无类型检查运行时不检查类型
Vue DevTools功能受限可查看但不完整
HMR不支持修改后需刷新页面
Source Map不支持调试时行号可能不准确

完整示例

列表页 (index.vue)

vue
<template>
  <div class="flex flex-col gap-3">
    <CatchTable
      :columns="columns"
      api="users"
      permission="user.user"
      :exports="true"
      :trash="true"
    >
      <template #avatar="scope">
        <Cavatar :avatar="scope.row.avatar" :size="35" />
      </template>
      <template #dialog="row">
        <Create :primary="row?.id" api="users" />
      </template>
    </CatchTable>
  </div>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
import Create from './create.vue'
import CatchTable from '@/components/catchTable/index.vue'
import Cavatar from '@/components/admin/avatar/cavatar.vue'
import { useUserStore } from '@/stores/modules/user'

const userStore = useUserStore()

const columns = [
  { type: 'selection' },
  { label: '用户名', prop: 'username' },
  { label: '头像', prop: 'avatar', slot: 'avatar' },
  { label: '邮箱', prop: 'email' },
  { label: '状态', prop: 'status', switch: true },
  { type: 'operate', label: '操作', width: 200 }
]
</script>

表单页 (create.vue)

vue
<template>
  <el-form :model="formData" label-width="100px" v-loading="loading">
    <el-form-item label="用户名" prop="username">
      <el-input v-model="formData.username" />
    </el-form-item>
    <el-form-item label="邮箱" prop="email">
      <el-input v-model="formData.email" />
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="submit">提交</el-button>
    </el-form-item>
  </el-form>
</template>

<script lang="ts" setup>
import { inject, onMounted } from 'vue'
import { useCreate } from '@/composables/curd/useCreate'
import { useShow } from '@/composables/curd/useShow'
import http from '@/support/http'

const props = defineProps<{
  primary?: number
  api: string
}>()

const closeDialog = inject('closeDialog') as () => void
const { formData, loading, create, update } = useCreate(props.api)
const { getDetail } = useShow(props.api)

onMounted(async () => {
  if (props.primary) {
    const data = await getDetail(props.primary)
    Object.assign(formData, data)
  }
})

const submit = async () => {
  if (props.primary) {
    await update(props.primary)
  } else {
    await create()
  }
  closeDialog()
}
</script>

性能最佳实践

1. 保持组件简单

远程组件在运行时编译,首次加载会有编译开销。建议:

vue
<!-- ✅ 推荐:简单的 UI 展示 -->
<template>
  <CatchTable :columns="columns" api="users" />
</template>

<!-- ❌ 避免:复杂的计算逻辑 -->
<script setup>
// 不建议在远程组件中做大量数据处理
const processedData = computed(() => {
  return largeDataSet.map(item => /* 复杂计算 */)
})
</script>

2. 合理拆分组件

将复杂页面拆分为多个小组件,每个组件职责单一:

view/user/
├─index.vue        # 主页面(简单)
├─create.vue       # 表单(简单)
└─components/
   ├─filter.vue    # 筛选器
   └─stats.vue     # 统计卡片

3. 避免重复导入

同一模块只导入一次:

vue
<script setup>
// ✅ 正确
import { ref, computed, watch } from 'vue'

// ❌ 避免
import { ref } from 'vue'
import { computed } from 'vue'
</script>

样式处理

Tailwind CSS

主应用已加载 Tailwind,远程组件可直接使用

vue
<template>
  <div class="flex flex-col gap-4 p-6 bg-white rounded-lg shadow">
    <h1 class="text-2xl font-bold text-gray-800">标题</h1>
    <p class="text-gray-600">描述文字</p>
  </div>
</template>

Scoped 样式

<style scoped> 正常工作:

vue
<style scoped>
.custom-card {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  padding: 16px;
}
</style>

行内样式

行内样式正常工作:

vue
<template>
  <div :style="{ backgroundColor: '#f3f4f6', padding: '20px' }">
    内容
  </div>
</template>

注意

不支持 SCSS/Less 等 CSS 预处理器语法。

调试技巧

1. 使用 console.log

最可靠的调试方式:

vue
<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  console.log('[UserIndex] 组件已挂载')
  console.log('[UserIndex] userStore:', userStore)
})
</script>

2. 检查网络请求

打开浏览器开发者工具 → Network 面板:

  • 检查 .vue 文件是否正确加载
  • 检查 API 请求是否正常

3. 查看控制台错误

常见错误类型:

  • Failed to resolve component → 未导入组件
  • Module not found → 模块路径错误
  • xxx is not defined → 变量/函数未定义

4. 使用 Vue DevTools

虽然功能受限,但仍可用于:

  • 查看组件树结构
  • 检查 props 和 data
  • 查看 Pinia store 状态

Inject/Provide

使用主应用提供的内容

CatchTable 组件提供了 closeDialog

vue
<script setup>
import { inject } from 'vue'

const closeDialog = inject('closeDialog') as () => void

const submit = () => {
  // 提交后关闭弹窗
  closeDialog()
}
</script>

在子组件间共享数据

vue
<!-- 父组件 -->
<script setup>
import { provide, ref } from 'vue'

const sharedData = ref({ count: 0 })
provide('sharedData', sharedData)
</script>

<!-- 子组件 -->
<script setup>
import { inject } from 'vue'

const sharedData = inject('sharedData')
</script>

常见问题

组件无法解析

问题:模板中使用的组件报 Failed to resolve component 错误

解决:确保在 <script setup> 中显式导入了所有使用的组件

vue
<script setup>
// ✅ 正确:显式导入
import CatchTable from '@/components/catchTable/index.vue'
import Cavatar from '@/components/admin/avatar/cavatar.vue'
</script>

<template>
  <CatchTable />
  <Cavatar />
</template>

模块未找到

问题:导入的模块报 Module not found 错误

解决:检查模块路径是否正确,确保使用 @/ 开头的路径别名

javascript
// ✅ 正确
import { useUserStore } from '@/stores/modules/user'

// ❌ 错误
import { useUserStore } from 'stores/modules/user'
import { useUserStore } from './stores/modules/user'

Store 状态不共享

问题:插件中的 store 与主应用的 store 状态不同步

解决:这是正常的——远程组件共享主应用的 Pinia 实例,store 状态是共享的。如果发现不同步,请检查是否正确导入了 store。

下一步