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>
23 KiB
23 KiB
组件使用文档
本文档详细介绍了 Telegram 营销管理系统中使用的各种组件及其使用方法。
目录
基础组件
1. BasicTable 基础表格
基础表格组件,提供完整的数据展示和操作功能。
基本用法
<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 基础表单
基础表单组件,支持动态表单生成和验证。
基本用法
<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 基础弹窗
基础弹窗组件,提供完整的弹窗功能。
基本用法
<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 账号的业务组件。
基本用法
<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: 选中的账号IDmultiple: 是否支持多选disabled: 是否禁用placeholder: 占位文本
2. GroupSelector 群组选择器
用于选择 Telegram 群组的业务组件。
基本用法
<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 消息模板
用于消息模板编辑和管理的组件。
基本用法
<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. 高级搜索表单
用于复杂条件搜索的表单组件。
<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. 动态表单
支持动态添加和删除字段的表单。
<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. 可编辑表格
支持行内编辑的表格组件。
<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. 树形表格
支持树形结构展示的表格。
<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 的图表组件封装。
<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. 实时图表
支持实时数据更新的图表组件。
<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. 基础文件上传
<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. 图片上传预览
<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. 文件拖拽上传
<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 指令控制元素显示。
<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. 权限组件
使用组件方式控制权限。
<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. 权限路由守卫
// 在路由配置中使用权限
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. 国际化文本
<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. 语言切换器
<template>
<LocalePicker :reload="true" :showText="true" />
</template>
<script setup lang="ts">
import { LocalePicker } from '@/components/I18n';
</script>
3. 表单国际化
<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 命名组件
- 组件文件名与组件名保持一致
- 业务组件添加业务前缀
// ✅ 正确
export default defineComponent({
name: 'UserAccountSelector',
// ...
});
// ❌ 错误
export default defineComponent({
name: 'userAccountSelector',
// ...
});
Props 定义
- 使用 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 定义事件
- 提供类型约束
- 添加事件说明
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 缓存组件
- 大列表使用虚拟滚动
- 图表组件按需加载
<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>
错误处理
- 组件内部处理异常
- 提供错误边界组件
- 用户友好的错误提示
<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 命名规范
- 支持主题自定义
<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. 测试建议
单元测试
- 测试组件的输入输出
- 测试用户交互
- 测试边界情况
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 文档
// 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: '请选择多个用户',
},
};
总结
本文档涵盖了系统中主要组件的使用方法和最佳实践。在实际开发中,建议:
- 遵循组件设计原则:单一职责、可复用、可测试
- 保持 API 一致性:统一的命名规范和参数格式
- 注重性能优化:合理使用缓存和懒加载
- 完善错误处理:提供友好的错误提示和恢复机制
- 编写完整文档:包括使用示例、API 说明和最佳实践
如有疑问或需要添加新的组件说明,请联系开发团队。