4.7 KiB
4.7 KiB
Component Scaffold — 组件模板
主流程见 SKILL.md,本文档为各类 Vue 3 组件的完整模板。
⚠️ 双前端区分:本文件中使用
el-*组件的模板仅适用于管理端 (Case-Database-Frontend-admin/)。 用户端 (Case-Database-Frontend-user/) 使用 Headless UI + Tailwind CSS,禁止引入 Element Plus。
基础 UI 组件
<script setup>
const props = defineProps({
title: { type: String, required: true },
loading: { type: Boolean, default: false },
})
const emit = defineEmits(['refresh'])
</script>
<template>
<div class="rounded-lg border border-gray-200 p-4" data-testid="component-name">
<h3 class="text-lg font-semibold mb-2">{{ props.title }}</h3>
<slot />
<el-button v-if="props.loading" :loading="true" class="mt-2" />
</div>
</template>
表格组件 (Element Plus)
<script setup>
import { useTable } from '@/hooks/useTable'
const props = defineProps({ apiUrl: { type: String, required: true }, columns: { type: Array, required: true } })
const { loading, dataList, pagination, loadData } = useTable((params) => request.get(props.apiUrl, { params }))
onMounted(() => loadData())
</script>
<template>
<div class="space-y-4">
<el-table v-loading="loading" :data="dataList" border stripe data-testid="data-table">
<el-table-column v-for="col in columns" :key="col.prop" v-bind="col" />
<el-table-column label="操作" fixed="right" width="180">
<template #default="{ row }"><slot name="actions" :row="row" /></template>
</el-table-column>
</el-table>
<el-pagination v-model:current-page="pagination.current" v-model:page-size="pagination.size" :total="pagination.total"
layout="total, sizes, prev, pager, next" @current-change="loadData" @size-change="loadData" />
</div>
</template>
表单对话框组件
<script setup>
const props = defineProps({ visible: { type: Boolean, required: true }, title: { type: String, required: true }, formData: { type: Object, default: () => ({}) } })
const emit = defineEmits(['update:visible', 'submit'])
const formRef = ref()
const form = reactive({ ...props.formData })
const rules = { name: [{ required: true, message: '请输入名称', trigger: 'blur' }] }
async function handleSubmit() { await formRef.value?.validate(); emit('submit', { ...form }) }
</script>
<template>
<el-dialog :model-value="visible" :title="title" width="600px" @update:model-value="emit('update:visible', $event)">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="名称" prop="name"><el-input v-model="form.name" placeholder="请输入" /></el-form-item>
<slot name="form-fields" :form="form" />
</el-form>
<template #footer>
<el-button @click="emit('update:visible', false)">取消</el-button>
<el-button type="primary" @click="handleSubmit">确认</el-button>
</template>
</el-dialog>
</template>
复合组件 (Compound + provide/inject)
Provider 组件:provide 状态 + actions。子组件:inject 消费。keys.ts 定义 Symbol + useDataPanel()。消费者组合:<DataPanelProvider :fetch-fn="orderApi.list"><DataPanelContent /></DataPanelProvider>
组件设计决策树
≥3 个布尔 prop 控制渲染?→ 拆分为显式变体组件
多个子组件共享状态?→ provide/inject 复合组件
业务逻辑 > 50 行?→ 提取 composable
>2 处复用?→ 放入 src/components/,使用 slot
否则 → 基础 UI 组件模板
逻辑提取规则 (CRITICAL)
纯转换逻辑必须提取到 ComponentName.utils.ts,组件只保留 UI 和事件。文件结构:ComponentName.vue + ComponentName.utils.ts + ComponentName.utils.test.ts + ComponentName.test.ts(可选)+ index.ts
复杂度阈值
| 指标 | 阈值 | 动作 |
|---|---|---|
| 组件总行数 | > 200 | ⚠️ 考虑拆分 |
| 组件总行数 | > 400 | 🔴 必须拆分 |
| script 逻辑行数 | > 50 | 提取 composable |
| 布尔 Props | ≥ 3 | 拆分为变体组件 |
Prop 反模式与显式变体
❌ 4 个布尔 = 16 种状态,v-if 地狱。✅ 显式变体:AdminEditPanel / PublicReadonlyPanel / DraftPreviewPanel,通过 slot 复用共享子组件。Slot vs Prop:简单数据用 Prop,自定义渲染用 Slot,控制显示用有无 slot 内容。
错误处理规范
| 场景 | 组件 |
|---|---|
| 操作成功/失败即时反馈 | ElMessage |
| 需用户确认 | ElMessageBox |
| 异步持续状态 | ElNotification |
| 页面级加载失败 | 内联错误 UI + 重试按钮 |
UI 设计反模式
居中一切、紫色渐变、大圆角、过度阴影、空白页大插图、过多动画 → 以 Element Plus 为基准,优先功能清晰度。