---
url: /docs/5.0/plugin/vue-component.md
---
# Vue 单页组件开发

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

## 概述

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

:::tip 💡 核心特性

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

## ⚠️ 重要限制

远程 SFC 组件是在**运行时**通过 [vue3-sfc-loader](https://github.com/FranckFreiburger/vue3-sfc-loader) 加载的，与主应用的编译时处理不同，因此存在以下限制：

:::warning 必须了解的限制

### 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'` | 代码编辑器 |

:::warning ⚠️ 注意
远程组件中使用的所有组件都必须**显式导入**，不能依赖主应用的自动导入功能。
:::

### 第三方库（懒加载）

| 模块 | 导入方式 | 说明 |
|------|----------|------|
| 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>
```

:::warning 注意
不支持 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。

## 下一步

* 了解 [插件生命周期钩子](./hook.md)
* 查看 [插件开发实战](./practice.md)
* 遇到问题？查看 [常见问题](./faq.md)
