主题
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>可用模块一览
核心库(预加载)
这些模块在应用启动时就已加载,可直接使用:
| 模块 | 导入方式 | 说明 |
|---|---|---|
| Vue | import { ref, computed } from 'vue' | Vue 3 核心 |
| Vue Router | import { useRouter } from 'vue-router' | 路由 |
| Pinia | import { storeToRefs } from 'pinia' | 状态管理 |
| Element Plus | import { ElMessage } from 'element-plus' | UI 组件库 |
| Element Plus Icons | import { Edit } from '@element-plus/icons-vue' | 图标 |
| Heroicons | import { UserIcon } from '@heroicons/vue/24/outline' | 图标 |
Stores(状态管理)
| 模块 | 导入方式 | 说明 |
|---|---|---|
| App Store | import { useAppStore } from '@/stores/modules/app' | 应用配置 |
| User Store | import { useUserStore } from '@/stores/modules/user' | 用户信息 |
| Permissions Store | import { usePermissionsStore } from '@/stores/modules/user/permissions' | 权限管理 |
| Tabs Store | import { 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 操作
| 模块 | 导入方式 | 说明 |
|---|---|---|
| useCreate | import { useCreate } from '@/composables/curd/useCreate' | 创建操作 |
| useShow | import { useShow } from '@/composables/curd/useShow' | 查看详情 |
| useDestroy | import { useDestroy } from '@/composables/curd/useDestroy' | 删除操作 |
| useEnabled | import { useEnabled } from '@/composables/curd/useEnabled' | 启用/禁用 |
| useGetList | import { useGetList } from '@/composables/curd/useGetList' | 获取列表 |
| useOpen | import { useOpen } from '@/composables/curd/useOpen' | 打开弹窗 |
| useRestore | import { useRestore } from '@/composables/curd/useRestore' | 恢复删除 |
| useFormSubmit | import { useFormSubmit } from '@/composables/curd/useFormSubmit' | 表单提交 |
| useExcelDownload | import { 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
| 模块 | 导入方式 | 说明 |
|---|---|---|
| useSse | import { useSse } from '@/composables/useSse' | SSE 实时通信 |
| useUpload | import { useUpload } from '@/composables/useUpload' | 文件上传 |
| useChunkUpload | import { useChunkUpload } from '@/composables/useChunkUpload' | 分片上传 |
| useDynamic | import { useDynamic } from '@/composables/useDynamic' | 动态组件 |
| useGetRemoteTableData | import { useGetRemoteTableData } from '@/composables/useGetRemoteTableData' | 远程表格数据 |
Support 工具模块
| 模块 | 导入方式 | 说明 |
|---|---|---|
| http | import http from '@/support/http' | HTTP 请求 |
| helper | import { isUndefined, isEmpty } from '@/support/helper' | 辅助函数 |
| message | import Message from '@/support/message' | 消息提示 |
| cache | import Cache from '@/support/cache' | 缓存管理 |
| request | import 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 目录下的组件都可以导入使用:
| 组件 | 导入方式 | 说明 |
|---|---|---|
| CatchTable | import CatchTable from '@/components/catchTable/index.vue' | 数据表格 |
| CatchForm | import CatchForm from '@/components/catchForm/index.vue' | 动态表单 |
| Cavatar | import Cavatar from '@/components/admin/avatar/cavatar.vue' | 头像组件 |
| Dialog | import Dialog from '@/components/admin/dialog/index.vue' | 弹窗组件 |
| Upload | import Upload from '@/components/admin/upload/index.vue' | 上传组件 |
| Icon | import Icon from '@/components/icon/index.vue' | 图标组件 |
| Cascader | import Cascader from '@/components/admin/cascader/index.vue' | 级联选择 |
| Select | import Select from '@/components/admin/select/index.vue' | 下拉选择 |
| Area | import Area from '@/components/admin/area/index.vue' | 地区选择 |
| Editor | import Editor from '@/components/editor/index.vue' | 富文本编辑器 |
| Code | import Code from '@/components/code/index.vue' | 代码编辑器 |
⚠️ 注意
远程组件中使用的所有组件都必须显式导入,不能依赖主应用的自动导入功能。
第三方库(懒加载)
| 模块 | 导入方式 | 说明 |
|---|---|---|
| VueUse | import { useMouse } from '@vueuse/core' | Vue 工具集 |
| Echarts | import * as echarts from 'echarts' | 图表库 |
| Vue Echarts | import VChart from 'vue-echarts' | Vue 图表组件 |
| Moment | import moment from 'moment' | 日期处理 |
| Vue I18n | import { 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。

