Files
2026-03-05 21:27:11 +08:00

4.7 KiB
Raw Permalink Blame History

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 为基准,优先功能清晰度。