Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1183 lines
23 KiB
Markdown
1183 lines
23 KiB
Markdown
# 组件使用文档
|
||
|
||
本文档详细介绍了 Telegram 营销管理系统中使用的各种组件及其使用方法。
|
||
|
||
## 目录
|
||
|
||
1. [基础组件](#基础组件)
|
||
2. [业务组件](#业务组件)
|
||
3. [表单组件](#表单组件)
|
||
4. [表格组件](#表格组件)
|
||
5. [图表组件](#图表组件)
|
||
6. [上传组件](#上传组件)
|
||
7. [权限组件](#权限组件)
|
||
8. [国际化组件](#国际化组件)
|
||
9. [最佳实践](#最佳实践)
|
||
|
||
## 基础组件
|
||
|
||
### 1. BasicTable 基础表格
|
||
|
||
基础表格组件,提供完整的数据展示和操作功能。
|
||
|
||
#### 基本用法
|
||
|
||
```vue
|
||
<template>
|
||
<BasicTable @register="registerTable" />
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { BasicTable, useTable } from '@/components/Table';
|
||
|
||
const columns = [
|
||
{
|
||
title: '用户名',
|
||
dataIndex: 'username',
|
||
key: 'username',
|
||
},
|
||
{
|
||
title: '邮箱',
|
||
dataIndex: 'email',
|
||
key: 'email',
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
key: 'status',
|
||
customRender: ({ record }) => {
|
||
return record.status === 1 ? '启用' : '禁用';
|
||
},
|
||
},
|
||
];
|
||
|
||
const [registerTable] = useTable({
|
||
title: '用户列表',
|
||
api: getUserList,
|
||
columns,
|
||
showIndexColumn: true,
|
||
showTableSetting: true,
|
||
useSearchForm: true,
|
||
formConfig: {
|
||
labelWidth: 80,
|
||
schemas: [
|
||
{
|
||
field: 'username',
|
||
label: '用户名',
|
||
component: 'Input',
|
||
colProps: { span: 8 },
|
||
},
|
||
],
|
||
},
|
||
actionColumn: {
|
||
title: '操作',
|
||
dataIndex: 'action',
|
||
slots: { customRender: 'action' },
|
||
fixed: 'right',
|
||
width: 120,
|
||
},
|
||
});
|
||
</script>
|
||
```
|
||
|
||
#### 主要配置项
|
||
|
||
- `api`: 数据请求接口函数
|
||
- `columns`: 表格列定义
|
||
- `showIndexColumn`: 是否显示序号列
|
||
- `showTableSetting`: 是否显示表格设置
|
||
- `useSearchForm`: 是否使用搜索表单
|
||
- `formConfig`: 搜索表单配置
|
||
- `actionColumn`: 操作列配置
|
||
|
||
### 2. BasicForm 基础表单
|
||
|
||
基础表单组件,支持动态表单生成和验证。
|
||
|
||
#### 基本用法
|
||
|
||
```vue
|
||
<template>
|
||
<BasicForm @register="register" @submit="handleSubmit" />
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { BasicForm, useForm } from '@/components/Form';
|
||
|
||
const schemas = [
|
||
{
|
||
field: 'username',
|
||
label: '用户名',
|
||
component: 'Input',
|
||
required: true,
|
||
componentProps: {
|
||
placeholder: '请输入用户名',
|
||
},
|
||
},
|
||
{
|
||
field: 'password',
|
||
label: '密码',
|
||
component: 'InputPassword',
|
||
required: true,
|
||
componentProps: {
|
||
placeholder: '请输入密码',
|
||
},
|
||
},
|
||
{
|
||
field: 'email',
|
||
label: '邮箱',
|
||
component: 'Input',
|
||
rules: [
|
||
{ required: true, message: '请输入邮箱' },
|
||
{ type: 'email', message: '邮箱格式不正确' },
|
||
],
|
||
},
|
||
];
|
||
|
||
const [register, { validate, setFieldsValue }] = useForm({
|
||
labelWidth: 100,
|
||
schemas,
|
||
showActionButtonGroup: true,
|
||
submitButtonOptions: {
|
||
text: '提交',
|
||
},
|
||
});
|
||
|
||
async function handleSubmit(values: any) {
|
||
try {
|
||
await someApi(values);
|
||
// 处理成功逻辑
|
||
} catch (error) {
|
||
// 处理错误
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
#### 支持的组件类型
|
||
|
||
- `Input`: 输入框
|
||
- `InputPassword`: 密码输入框
|
||
- `InputNumber`: 数字输入框
|
||
- `InputTextArea`: 文本域
|
||
- `Select`: 选择器
|
||
- `CheckboxGroup`: 多选框组
|
||
- `RadioButtonGroup`: 单选按钮组
|
||
- `DatePicker`: 日期选择器
|
||
- `RangePicker`: 日期范围选择器
|
||
- `Upload`: 上传组件
|
||
|
||
### 3. BasicModal 基础弹窗
|
||
|
||
基础弹窗组件,提供完整的弹窗功能。
|
||
|
||
#### 基本用法
|
||
|
||
```vue
|
||
<template>
|
||
<BasicModal @register="registerModal" title="用户详情" @ok="handleSubmit">
|
||
<BasicForm @register="registerForm" />
|
||
</BasicModal>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { BasicModal, useModalInner } from '@/components/Modal';
|
||
import { BasicForm, useForm } from '@/components/Form';
|
||
|
||
const [registerForm, { validate, setFieldsValue }] = useForm({
|
||
schemas: [
|
||
{
|
||
field: 'username',
|
||
label: '用户名',
|
||
component: 'Input',
|
||
required: true,
|
||
},
|
||
],
|
||
});
|
||
|
||
const [registerModal, { setModalProps, closeModal }] = useModalInner(
|
||
async (data) => {
|
||
// 接收父组件传入的数据
|
||
if (data.record) {
|
||
setFieldsValue(data.record);
|
||
}
|
||
},
|
||
);
|
||
|
||
async function handleSubmit() {
|
||
try {
|
||
const values = await validate();
|
||
await updateUser(values);
|
||
closeModal();
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
## 业务组件
|
||
|
||
### 1. AccountSelector 账号选择器
|
||
|
||
用于选择 Telegram 账号的业务组件。
|
||
|
||
#### 基本用法
|
||
|
||
```vue
|
||
<template>
|
||
<AccountSelector v-model:value="selectedAccounts" multiple />
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue';
|
||
import { AccountSelector } from '@/components/Business';
|
||
|
||
const selectedAccounts = ref<string[]>([]);
|
||
</script>
|
||
```
|
||
|
||
#### 属性配置
|
||
|
||
- `value`: 选中的账号ID
|
||
- `multiple`: 是否支持多选
|
||
- `disabled`: 是否禁用
|
||
- `placeholder`: 占位文本
|
||
|
||
### 2. GroupSelector 群组选择器
|
||
|
||
用于选择 Telegram 群组的业务组件。
|
||
|
||
#### 基本用法
|
||
|
||
```vue
|
||
<template>
|
||
<GroupSelector
|
||
v-model:value="selectedGroups"
|
||
:account-id="accountId"
|
||
show-member-count
|
||
/>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue';
|
||
import { GroupSelector } from '@/components/Business';
|
||
|
||
const selectedGroups = ref<string[]>([]);
|
||
const accountId = ref('account-123');
|
||
</script>
|
||
```
|
||
|
||
### 3. MessageTemplate 消息模板
|
||
|
||
用于消息模板编辑和管理的组件。
|
||
|
||
#### 基本用法
|
||
|
||
```vue
|
||
<template>
|
||
<MessageTemplate
|
||
v-model:content="messageContent"
|
||
:variables="templateVariables"
|
||
@preview="handlePreview"
|
||
/>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue';
|
||
import { MessageTemplate } from '@/components/Business';
|
||
|
||
const messageContent = ref('');
|
||
const templateVariables = ref([
|
||
{ key: 'name', label: '姓名', default: '用户' },
|
||
{ key: 'product', label: '产品', default: '产品名称' },
|
||
]);
|
||
|
||
function handlePreview(content: string) {
|
||
console.log('预览内容:', content);
|
||
}
|
||
</script>
|
||
```
|
||
|
||
## 表单组件
|
||
|
||
### 1. 高级搜索表单
|
||
|
||
用于复杂条件搜索的表单组件。
|
||
|
||
```vue
|
||
<template>
|
||
<div class="search-form-container">
|
||
<BasicForm @register="registerSearchForm" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { BasicForm, useForm } from '@/components/Form';
|
||
|
||
const searchSchemas = [
|
||
{
|
||
field: 'keyword',
|
||
label: '关键字',
|
||
component: 'Input',
|
||
componentProps: {
|
||
placeholder: '请输入搜索关键字',
|
||
},
|
||
colProps: { span: 8 },
|
||
},
|
||
{
|
||
field: 'status',
|
||
label: '状态',
|
||
component: 'Select',
|
||
componentProps: {
|
||
options: [
|
||
{ label: '全部', value: '' },
|
||
{ label: '启用', value: 1 },
|
||
{ label: '禁用', value: 0 },
|
||
],
|
||
},
|
||
colProps: { span: 8 },
|
||
},
|
||
{
|
||
field: 'dateRange',
|
||
label: '时间范围',
|
||
component: 'RangePicker',
|
||
componentProps: {
|
||
format: 'YYYY-MM-DD',
|
||
valueFormat: 'YYYY-MM-DD',
|
||
},
|
||
colProps: { span: 8 },
|
||
},
|
||
];
|
||
|
||
const [registerSearchForm, { getFieldsValue, resetFields }] = useForm({
|
||
labelWidth: 80,
|
||
schemas: searchSchemas,
|
||
showActionButtonGroup: true,
|
||
submitButtonOptions: {
|
||
text: '搜索',
|
||
},
|
||
resetButtonOptions: {
|
||
text: '重置',
|
||
},
|
||
autoSubmitOnEnter: true,
|
||
});
|
||
</script>
|
||
```
|
||
|
||
### 2. 动态表单
|
||
|
||
支持动态添加和删除字段的表单。
|
||
|
||
```vue
|
||
<template>
|
||
<BasicForm @register="registerDynamicForm" />
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { reactive } from 'vue';
|
||
import { BasicForm, useForm } from '@/components/Form';
|
||
|
||
const dynamicSchemas = reactive([
|
||
{
|
||
field: 'contacts',
|
||
label: '联系人列表',
|
||
component: 'Input',
|
||
slot: 'contactsSlot',
|
||
},
|
||
]);
|
||
|
||
const [registerDynamicForm] = useForm({
|
||
schemas: dynamicSchemas,
|
||
showActionButtonGroup: false,
|
||
});
|
||
|
||
// 动态添加联系人字段
|
||
function addContact() {
|
||
const index = Math.random().toString(36).substr(2, 9);
|
||
dynamicSchemas.push({
|
||
field: `contact_${index}`,
|
||
label: `联系人${dynamicSchemas.length}`,
|
||
component: 'Input',
|
||
componentProps: {
|
||
placeholder: '请输入联系人信息',
|
||
},
|
||
});
|
||
}
|
||
</script>
|
||
```
|
||
|
||
## 表格组件
|
||
|
||
### 1. 可编辑表格
|
||
|
||
支持行内编辑的表格组件。
|
||
|
||
```vue
|
||
<template>
|
||
<BasicTable @register="registerEditTable">
|
||
<template #bodyCell="{ column, record, index }">
|
||
<template v-if="column.dataIndex === 'name'">
|
||
<Input
|
||
v-if="editingKey === record.key"
|
||
v-model:value="record.name"
|
||
@pressEnter="save(record.key)"
|
||
/>
|
||
<span v-else>{{ record.name }}</span>
|
||
</template>
|
||
|
||
<template v-if="column.dataIndex === 'action'">
|
||
<span v-if="editingKey === record.key">
|
||
<Button type="link" @click="save(record.key)">保存</Button>
|
||
<Button type="link" @click="cancel">取消</Button>
|
||
</span>
|
||
<span v-else>
|
||
<Button type="link" @click="edit(record.key)">编辑</Button>
|
||
</span>
|
||
</template>
|
||
</template>
|
||
</BasicTable>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue';
|
||
import { BasicTable, useTable } from '@/components/Table';
|
||
import { Button, Input } from 'ant-design-vue';
|
||
|
||
const editingKey = ref('');
|
||
|
||
const columns = [
|
||
{
|
||
title: '姓名',
|
||
dataIndex: 'name',
|
||
key: 'name',
|
||
},
|
||
{
|
||
title: '操作',
|
||
dataIndex: 'action',
|
||
key: 'action',
|
||
},
|
||
];
|
||
|
||
const [registerEditTable, { getDataSource, setTableData }] = useTable({
|
||
columns,
|
||
dataSource: [],
|
||
showIndexColumn: false,
|
||
pagination: false,
|
||
});
|
||
|
||
function edit(key: string) {
|
||
editingKey.value = key;
|
||
}
|
||
|
||
function save(key: string) {
|
||
editingKey.value = '';
|
||
// 保存逻辑
|
||
}
|
||
|
||
function cancel() {
|
||
editingKey.value = '';
|
||
// 取消编辑,恢复原始数据
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### 2. 树形表格
|
||
|
||
支持树形结构展示的表格。
|
||
|
||
```vue
|
||
<template>
|
||
<BasicTable @register="registerTreeTable" />
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { BasicTable, useTable } from '@/components/Table';
|
||
|
||
const treeColumns = [
|
||
{
|
||
title: '名称',
|
||
dataIndex: 'name',
|
||
key: 'name',
|
||
},
|
||
{
|
||
title: '类型',
|
||
dataIndex: 'type',
|
||
key: 'type',
|
||
},
|
||
];
|
||
|
||
const [registerTreeTable] = useTable({
|
||
columns: treeColumns,
|
||
api: getTreeData,
|
||
isTreeTable: true,
|
||
pagination: false,
|
||
childrenColumnName: 'children',
|
||
indentSize: 20,
|
||
});
|
||
</script>
|
||
```
|
||
|
||
## 图表组件
|
||
|
||
### 1. 基础图表
|
||
|
||
基于 ECharts 的图表组件封装。
|
||
|
||
```vue
|
||
<template>
|
||
<div class="chart-container">
|
||
<EChart :option="chartOption" :height="400" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed } from 'vue';
|
||
import { EChart } from '@/components/Chart';
|
||
|
||
const chartData = ref([
|
||
{ name: '周一', value: 120 },
|
||
{ name: '周二', value: 200 },
|
||
{ name: '周三', value: 150 },
|
||
{ name: '周四', value: 80 },
|
||
{ name: '周五', value: 70 },
|
||
{ name: '周六', value: 110 },
|
||
{ name: '周日', value: 130 },
|
||
]);
|
||
|
||
const chartOption = computed(() => ({
|
||
title: {
|
||
text: '每日发送量统计',
|
||
left: 'center',
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: chartData.value.map((item) => item.name),
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
},
|
||
series: [
|
||
{
|
||
name: '发送量',
|
||
type: 'line',
|
||
data: chartData.value.map((item) => item.value),
|
||
smooth: true,
|
||
},
|
||
],
|
||
}));
|
||
</script>
|
||
```
|
||
|
||
### 2. 实时图表
|
||
|
||
支持实时数据更新的图表组件。
|
||
|
||
```vue
|
||
<template>
|
||
<div class="realtime-chart">
|
||
<EChart :option="realtimeOption" :height="300" />
|
||
<div class="chart-controls">
|
||
<Button @click="startRealtime">开始实时更新</Button>
|
||
<Button @click="stopRealtime">停止更新</Button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onUnmounted } from 'vue';
|
||
import { EChart } from '@/components/Chart';
|
||
import { Button } from 'ant-design-vue';
|
||
|
||
const realtimeData = ref<number[]>([]);
|
||
const categories = ref<string[]>([]);
|
||
let intervalId: NodeJS.Timeout | null = null;
|
||
|
||
const realtimeOption = computed(() => ({
|
||
xAxis: {
|
||
type: 'category',
|
||
data: categories.value,
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
},
|
||
series: [
|
||
{
|
||
name: '实时数据',
|
||
type: 'line',
|
||
data: realtimeData.value,
|
||
smooth: true,
|
||
},
|
||
],
|
||
}));
|
||
|
||
function startRealtime() {
|
||
if (intervalId) return;
|
||
|
||
intervalId = setInterval(() => {
|
||
const now = new Date();
|
||
const timeStr = now.toLocaleTimeString();
|
||
const value = Math.floor(Math.random() * 100);
|
||
|
||
realtimeData.value.push(value);
|
||
categories.value.push(timeStr);
|
||
|
||
// 保持最近20个数据点
|
||
if (realtimeData.value.length > 20) {
|
||
realtimeData.value.shift();
|
||
categories.value.shift();
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
function stopRealtime() {
|
||
if (intervalId) {
|
||
clearInterval(intervalId);
|
||
intervalId = null;
|
||
}
|
||
}
|
||
|
||
onUnmounted(() => {
|
||
stopRealtime();
|
||
});
|
||
</script>
|
||
```
|
||
|
||
## 上传组件
|
||
|
||
### 1. 基础文件上传
|
||
|
||
```vue
|
||
<template>
|
||
<BasicUpload
|
||
v-model:value="fileList"
|
||
:api="uploadApi"
|
||
:maxCount="5"
|
||
:maxSize="10"
|
||
accept=".jpg,.png,.pdf"
|
||
@change="handleChange"
|
||
/>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue';
|
||
import { BasicUpload } from '@/components/Upload';
|
||
|
||
const fileList = ref([]);
|
||
|
||
function handleChange(files: any[]) {
|
||
console.log('文件列表变化:', files);
|
||
}
|
||
|
||
async function uploadApi(params: any) {
|
||
// 自定义上传逻辑
|
||
const formData = new FormData();
|
||
formData.append('file', params.file);
|
||
|
||
const response = await fetch('/api/upload', {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
return response.json();
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### 2. 图片上传预览
|
||
|
||
```vue
|
||
<template>
|
||
<ImageUpload
|
||
v-model:value="imageList"
|
||
:preview="true"
|
||
:multiple="true"
|
||
:maxCount="10"
|
||
@preview="handlePreview"
|
||
/>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue';
|
||
import { ImageUpload } from '@/components/Upload';
|
||
|
||
const imageList = ref([]);
|
||
|
||
function handlePreview(file: any) {
|
||
console.log('预览图片:', file);
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### 3. 文件拖拽上传
|
||
|
||
```vue
|
||
<template>
|
||
<DraggerUpload
|
||
v-model:value="draggerFiles"
|
||
:multiple="true"
|
||
accept=".xlsx,.csv"
|
||
@success="handleUploadSuccess"
|
||
>
|
||
<p class="ant-upload-drag-icon">
|
||
<InboxOutlined />
|
||
</p>
|
||
<p class="ant-upload-text">点击或拖拽文件到此处上传</p>
|
||
<p class="ant-upload-hint">支持单个或批量上传,严禁上传敏感数据</p>
|
||
</DraggerUpload>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue';
|
||
import { DraggerUpload } from '@/components/Upload';
|
||
import { InboxOutlined } from '@ant-design/icons-vue';
|
||
|
||
const draggerFiles = ref([]);
|
||
|
||
function handleUploadSuccess(response: any) {
|
||
console.log('上传成功:', response);
|
||
}
|
||
</script>
|
||
```
|
||
|
||
## 权限组件
|
||
|
||
### 1. 权限指令
|
||
|
||
使用 v-permission 指令控制元素显示。
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<!-- 单个权限 -->
|
||
<Button v-permission="'user:create'" type="primary"> 新增用户 </Button>
|
||
|
||
<!-- 多个权限(满足任意一个) -->
|
||
<Button v-permission="['user:edit', 'user:delete']" type="default">
|
||
编辑用户
|
||
</Button>
|
||
|
||
<!-- 角色权限 -->
|
||
<Button v-role="'admin'" danger> 删除用户 </Button>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
### 2. 权限组件
|
||
|
||
使用组件方式控制权限。
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<Authority :value="'user:create'">
|
||
<Button type="primary">新增用户</Button>
|
||
</Authority>
|
||
|
||
<Authority :value="['user:edit', 'user:delete']" mode="any">
|
||
<Button type="default">编辑用户</Button>
|
||
</Authority>
|
||
|
||
<RoleAuthority :value="'admin'">
|
||
<Button danger>管理员功能</Button>
|
||
</RoleAuthority>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { Authority, RoleAuthority } from '@/components/Authority';
|
||
import { Button } from 'ant-design-vue';
|
||
</script>
|
||
```
|
||
|
||
### 3. 权限路由守卫
|
||
|
||
```typescript
|
||
// 在路由配置中使用权限
|
||
export const routes = [
|
||
{
|
||
path: '/user',
|
||
component: UserLayout,
|
||
meta: {
|
||
title: '用户管理',
|
||
permissions: ['user:view'], // 需要的权限
|
||
roles: ['admin', 'user'], // 允许的角色
|
||
},
|
||
children: [
|
||
{
|
||
path: 'list',
|
||
component: UserList,
|
||
meta: {
|
||
title: '用户列表',
|
||
permissions: ['user:list'],
|
||
},
|
||
},
|
||
{
|
||
path: 'create',
|
||
component: UserCreate,
|
||
meta: {
|
||
title: '新增用户',
|
||
permissions: ['user:create'],
|
||
},
|
||
},
|
||
],
|
||
},
|
||
];
|
||
```
|
||
|
||
## 国际化组件
|
||
|
||
### 1. 国际化文本
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<!-- 基本用法 -->
|
||
<h1>{{ $t('common.title') }}</h1>
|
||
|
||
<!-- 带参数 -->
|
||
<p>{{ $t('common.welcome', { name: userName }) }}</p>
|
||
|
||
<!-- 使用组件 -->
|
||
<I18nText keypath="common.description" />
|
||
|
||
<!-- 复杂格式化 -->
|
||
<I18nText keypath="common.count" :count="itemCount">
|
||
<template #count>
|
||
<strong>{{ itemCount }}</strong>
|
||
</template>
|
||
</I18nText>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue';
|
||
import { I18nText } from '@/components/I18n';
|
||
|
||
const userName = ref('张三');
|
||
const itemCount = ref(10);
|
||
</script>
|
||
```
|
||
|
||
### 2. 语言切换器
|
||
|
||
```vue
|
||
<template>
|
||
<LocalePicker :reload="true" :showText="true" />
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { LocalePicker } from '@/components/I18n';
|
||
</script>
|
||
```
|
||
|
||
### 3. 表单国际化
|
||
|
||
```vue
|
||
<template>
|
||
<BasicForm @register="registerI18nForm" />
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { BasicForm, useForm } from '@/components/Form';
|
||
import { useI18n } from 'vue-i18n';
|
||
|
||
const { t } = useI18n();
|
||
|
||
const i18nSchemas = [
|
||
{
|
||
field: 'name',
|
||
label: t('form.name'),
|
||
component: 'Input',
|
||
componentProps: {
|
||
placeholder: t('form.namePlaceholder'),
|
||
},
|
||
rules: [{ required: true, message: t('form.nameRequired') }],
|
||
},
|
||
{
|
||
field: 'email',
|
||
label: t('form.email'),
|
||
component: 'Input',
|
||
componentProps: {
|
||
placeholder: t('form.emailPlaceholder'),
|
||
},
|
||
rules: [
|
||
{ required: true, message: t('form.emailRequired') },
|
||
{ type: 'email', message: t('form.emailInvalid') },
|
||
],
|
||
},
|
||
];
|
||
|
||
const [registerI18nForm] = useForm({
|
||
labelWidth: 100,
|
||
schemas: i18nSchemas,
|
||
});
|
||
</script>
|
||
```
|
||
|
||
## 最佳实践
|
||
|
||
### 1. 组件开发规范
|
||
|
||
#### 组件命名
|
||
|
||
- 使用 PascalCase 命名组件
|
||
- 组件文件名与组件名保持一致
|
||
- 业务组件添加业务前缀
|
||
|
||
```typescript
|
||
// ✅ 正确
|
||
export default defineComponent({
|
||
name: 'UserAccountSelector',
|
||
// ...
|
||
});
|
||
|
||
// ❌ 错误
|
||
export default defineComponent({
|
||
name: 'userAccountSelector',
|
||
// ...
|
||
});
|
||
```
|
||
|
||
#### Props 定义
|
||
|
||
- 使用 TypeScript 类型定义
|
||
- 提供默认值和验证
|
||
- 添加注释说明
|
||
|
||
```typescript
|
||
interface Props {
|
||
/** 选中的值 */
|
||
value?: string | string[];
|
||
/** 是否支持多选 */
|
||
multiple?: boolean;
|
||
/** 是否禁用 */
|
||
disabled?: boolean;
|
||
/** 占位文本 */
|
||
placeholder?: string;
|
||
/** 最大选择数量 */
|
||
maxCount?: number;
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
value: undefined,
|
||
multiple: false,
|
||
disabled: false,
|
||
placeholder: '请选择',
|
||
maxCount: 10,
|
||
});
|
||
```
|
||
|
||
#### 事件定义
|
||
|
||
- 使用 defineEmits 定义事件
|
||
- 提供类型约束
|
||
- 添加事件说明
|
||
|
||
```typescript
|
||
interface Emits {
|
||
/** 值变化事件 */
|
||
(e: 'update:value', value: string | string[]): void;
|
||
/** 选择变化事件 */
|
||
(e: 'change', value: string | string[], option: any): void;
|
||
/** 清空事件 */
|
||
(e: 'clear'): void;
|
||
}
|
||
|
||
const emit = defineEmits<Emits>();
|
||
```
|
||
|
||
### 2. 组件使用最佳实践
|
||
|
||
#### 性能优化
|
||
|
||
- 合理使用 v-memo 缓存组件
|
||
- 大列表使用虚拟滚动
|
||
- 图表组件按需加载
|
||
|
||
```vue
|
||
<template>
|
||
<!-- 使用 v-memo 缓存 -->
|
||
<div v-memo="[user.id, user.status]">
|
||
<UserCard :user="user" />
|
||
</div>
|
||
|
||
<!-- 虚拟滚动大列表 -->
|
||
<VirtualList :items="userList" :item-height="60" :height="400">
|
||
<template #item="{ item, index }">
|
||
<UserItem :user="item" :index="index" />
|
||
</template>
|
||
</VirtualList>
|
||
</template>
|
||
```
|
||
|
||
#### 错误处理
|
||
|
||
- 组件内部处理异常
|
||
- 提供错误边界组件
|
||
- 用户友好的错误提示
|
||
|
||
```vue
|
||
<template>
|
||
<ErrorBoundary>
|
||
<Suspense>
|
||
<template #default>
|
||
<AsyncComponent />
|
||
</template>
|
||
<template #fallback>
|
||
<Loading />
|
||
</template>
|
||
</Suspense>
|
||
|
||
<template #error="{ error, retry }">
|
||
<ErrorDisplay :error="error" @retry="retry" />
|
||
</template>
|
||
</ErrorBoundary>
|
||
</template>
|
||
```
|
||
|
||
#### 样式管理
|
||
|
||
- 使用 CSS Modules 或 Scoped CSS
|
||
- 遵循 BEM 命名规范
|
||
- 支持主题自定义
|
||
|
||
```vue
|
||
<template>
|
||
<div :class="$style.container">
|
||
<div :class="$style.header">
|
||
<h3 :class="$style.title">{{ title }}</h3>
|
||
</div>
|
||
<div :class="$style.content">
|
||
<slot />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style module>
|
||
.container {
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.header {
|
||
padding: 16px;
|
||
border-bottom: 1px solid var(--border-color);
|
||
background-color: var(--header-bg);
|
||
}
|
||
|
||
.title {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.content {
|
||
padding: 16px;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
### 3. 测试建议
|
||
|
||
#### 单元测试
|
||
|
||
- 测试组件的输入输出
|
||
- 测试用户交互
|
||
- 测试边界情况
|
||
|
||
```typescript
|
||
import { mount } from '@vue/test-utils';
|
||
import { describe, it, expect } from 'vitest';
|
||
import UserSelector from '../UserSelector.vue';
|
||
|
||
describe('UserSelector', () => {
|
||
it('should render correctly', () => {
|
||
const wrapper = mount(UserSelector, {
|
||
props: {
|
||
value: ['user1', 'user2'],
|
||
multiple: true,
|
||
},
|
||
});
|
||
|
||
expect(wrapper.exists()).toBe(true);
|
||
expect(wrapper.findAll('.user-item')).toHaveLength(2);
|
||
});
|
||
|
||
it('should emit change event when selection changes', async () => {
|
||
const wrapper = mount(UserSelector);
|
||
|
||
await wrapper.find('.user-option').trigger('click');
|
||
|
||
expect(wrapper.emitted('change')).toBeTruthy();
|
||
expect(wrapper.emitted('update:value')).toBeTruthy();
|
||
});
|
||
});
|
||
```
|
||
|
||
#### 组件文档
|
||
|
||
- 使用 Storybook 展示组件
|
||
- 提供使用示例
|
||
- 记录 API 文档
|
||
|
||
```typescript
|
||
// UserSelector.stories.ts
|
||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||
import UserSelector from './UserSelector.vue';
|
||
|
||
const meta: Meta<typeof UserSelector> = {
|
||
title: 'Components/UserSelector',
|
||
component: UserSelector,
|
||
parameters: {
|
||
docs: {
|
||
description: {
|
||
component: '用户选择器组件,支持单选和多选模式',
|
||
},
|
||
},
|
||
},
|
||
argTypes: {
|
||
multiple: {
|
||
control: 'boolean',
|
||
description: '是否支持多选',
|
||
},
|
||
disabled: {
|
||
control: 'boolean',
|
||
description: '是否禁用',
|
||
},
|
||
},
|
||
};
|
||
|
||
export default meta;
|
||
type Story = StoryObj<typeof meta>;
|
||
|
||
export const Default: Story = {
|
||
args: {
|
||
multiple: false,
|
||
placeholder: '请选择用户',
|
||
},
|
||
};
|
||
|
||
export const Multiple: Story = {
|
||
args: {
|
||
multiple: true,
|
||
placeholder: '请选择多个用户',
|
||
},
|
||
};
|
||
```
|
||
|
||
## 总结
|
||
|
||
本文档涵盖了系统中主要组件的使用方法和最佳实践。在实际开发中,建议:
|
||
|
||
1. **遵循组件设计原则**:单一职责、可复用、可测试
|
||
2. **保持 API 一致性**:统一的命名规范和参数格式
|
||
3. **注重性能优化**:合理使用缓存和懒加载
|
||
4. **完善错误处理**:提供友好的错误提示和恢复机制
|
||
5. **编写完整文档**:包括使用示例、API 说明和最佳实践
|
||
|
||
如有疑问或需要添加新的组件说明,请联系开发团队。
|