基于vue3.0和element-plus的组件库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

687 lines
18 KiB

# Base Components
> Base Vue 3 components built on Element Plus.
---
## Overview
The `packages/base/` directory provides foundational Vue 3 components that wrap Element Plus components with theme integration, i18n support, and consistent prop patterns.
### Architecture
```
packages/base/
├── index.ts # Exports all base components
├── item/ # Basic UI elements
│ ├── tag.vue
│ ├── button.vue
│ ├── select.vue
│ ├── input.vue
│ ├── datetime.vue
│ ├── light-box.vue
│ ├── buttonWithTooltip.vue
│ ├── confirmCancel.vue
│ ├── tzDatePicker.vue
│ ├── tzDateTime.vue
│ └── ws-monitor-toggle.vue
└── data/ # Data-driven components
├── search-row.vue
├── list-table.vue
├── listTableDialog.vue
├── infomation.vue
├── modify-form.vue
├── descriptions.vue
└── table-action.vue
```
---
## Item Components
### NoobTag
Wraps `el-tag` with theme integration.
**File**: `packages/base/item/tag.vue`
**Props**:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `type` | `string` | `null` | Element Plus tag type |
| `color` | `string` | `null` | Custom color |
| `closable` | `boolean` | `false` | Show close button |
| `round` | `boolean` | `false` | Round style |
| `effect` | `string` | `'light'` | Tag effect (light/dark/plain) |
**Usage**:
```vue
<NoobTag type="success">Active</NoobTag>
<NoobTag color="#387dff">Custom</NoobTag>
<NoobTag closable round>Removable</NoobTag>
```
---
### NoobButton
Wraps `el-button` with theme integration and slot content detection.
**File**: `packages/base/item/button.vue`
**Props**:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `type` | `string` | `null` | Button type (primary/success/warning/danger/info) |
| `icon` | `string` | `null` | Icon name |
| `disabled` | `boolean` | `false` | Disabled state |
| `plain` | `boolean` | `false` | Plain style |
| `round` | `boolean` | `false` | Round style |
| `circle` | `boolean` | `false` | Circle style (icon-only) |
| `loading` | `boolean` | `false` | Loading state |
| `text` | `boolean` | `false` | Text button |
| `link` | `boolean` | `false` | Link button |
**Usage**:
```vue
<NoobButton type="primary" icon="Plus">Add</NoobButton>
<NoobButton type="danger" icon="Delete" circle />
<NoobButton loading @click="handleSave">Saving...</NoobButton>
```
**Note**: Automatically detects if slot content exists before rendering, handling empty slots in circle buttons.
---
### NoobSelect
Wraps `el-select` with Vuex dict integration and width management.
**File**: `packages/base/item/select.vue`
**Props**:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `modelValue` | `any` | `-` | v-model value |
| `placeholder` | `string` | `i18n: rule.pleaseSelect` | Placeholder text |
| `disabled` | `boolean` | `false` | Disabled state |
| `clearable` | `boolean` | `true` | Show clear button |
| `filterable` | `boolean` | `true` | Enable search |
| `full` | `boolean` | `false` | 100% width |
| `width` | `number` | `-` | Fixed width in px |
| `dict` | `string` | `-` | Vuex dict key for options |
| `stateProp` | `string` | `-` | Vuex state property for options |
| `maxValue` | `number` | `-` | Generate 1-N options |
| `valueKey` | `string` | `'key'` | Option value key |
| `labelKey` | `string` | `'value'` | Option label key |
| `remote` | `boolean` | `false` | Enable remote search |
| `remoteMethod` | `function` | `null` | Remote search function |
**Usage**:
```vue
<!-- Dict-based options -->
<NoobSelect v-model="form.status" dict="userStatus" />
<!-- Array options from Vuex state -->
<NoobSelect v-model="form.type" stateProp="userTypes" valueKey="id" labelKey="name" />
<!-- Numeric range options -->
<NoobSelect v-model="form.count" :maxValue="10" />
```
---
### NoobInput
Wraps `el-input` with width management and label prepend support.
**File**: `packages/base/item/input.vue`
**Props**:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `modelValue` | `any` | `-` | v-model value |
| `label` | `string` | `null` | Prepend label |
| `placeholder` | `string` | `i18n: rule.pleaseEnter` | Placeholder text |
| `type` | `string` | `null` | Input type |
| `disabled` | `boolean` | `false` | Disabled state |
| `clearable` | `boolean` | `true` | Show clear button |
| `full` | `boolean` | `false` | 100% width |
| `width` | `number` | `-` | Fixed width in px |
**Usage**:
```vue
<NoobInput v-model="form.name" label="Name" />
<NoobInput v-model="form.note" type="textarea" :rows="3" />
<NoobInput v-model="form.search" full placeholder="Search..." />
```
---
### ConfirmCancel
Confirmation button pair with i18n text.
**File**: `packages/base/item/confirmCancel.vue`
**Events**:
| Event | Payload | Description |
|-------|---------|-------------|
| `confirm` | `void` | Confirm button clicked |
| `cancel` | `void` | Cancel button clicked |
**Usage**:
```vue
<ConfirmCancel @confirm="handleConfirm" @cancel="handleCancel" />
```
---
### ButtonWithTooltip
Wraps `NoobButton` with tooltip disabled state handling.
**File**: `packages/base/item/buttonWithTooltip.vue`
**Props**:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `tooltip` | `string` | `-` | Tooltip content |
| `disabled` | `boolean` | `-` | Button disabled state |
**Usage**:
```vue
<ButtonWithTooltip tooltip="Requires admin access" :disabled="!isAdmin">
Delete
</ButtonWithTooltip>
```
---
### WsMonitorToggle
WebSocket subscription monitor with automatic retry logic.
**File**: `packages/base/item/ws-monitor-toggle.vue`
**Props**:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `topics` | `string[]` | **required** | Topics to monitor |
| `statusEndpoint` | `string` | `'/public/ws/topics'` | Status check endpoint |
| `interval` | `number` | `3000` | Retry interval (ms) |
| `maxRetries` | `number` | `10` | Max connection attempts |
| `connect` | `object/function` | `-` | Subscribe command or function |
| `disconnect` | `object/function` | `-` | Unsubscribe command or function |
| `disabled` | `boolean` | `false` | Disable toggle |
| `labelConnected` | `string` | `'ws.autorefresh.enabled'` | Connected label i18n key |
| `labelConnecting` | `string` | `'ws.autorefresh.enabling'` | Connecting label i18n key |
| `labelDisconnected` | `string` | `'ws.autorefresh.disabled'` | Disconnected label i18n key |
| `labelError` | `string` | `'ws.autorefresh.error'` | Error label i18n key |
**Events**:
| Event | Payload | Description |
|-------|---------|-------------|
| `status-change` | `boolean` | Connection status changed |
| `error` | `Error` | Error occurred |
**States**: `disconnected` | `connecting` | `connected` | `error`
**Usage**:
```vue
<WsMonitorToggle
:topics="['user:notifications', 'system:alerts']"
:connect="{ action: 'subscribe', topics: topics }"
:disconnect="{ action: 'unsubscribe', topics: topics }"
@status-change="onStatusChange"
/>
```
---
## Data Components
### SearchRow
Search form row with standard action buttons.
**File**: `packages/base/data/search-row.vue`
**Props**:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `title` | `string` | `null` | Row title |
| `fresh` | `boolean` | `true` | Show refresh button |
| `add` | `boolean` | `true` | Show add button |
| `del` | `boolean` | `true` | Show delete button |
| `query` | `boolean` | `true` | Show query button |
**Slots**:
| Slot | Description |
|------|-------------|
| `left` | Extra buttons on left side |
| `default` | Search form fields |
**Events**:
| Event | Description |
|-------|-------------|
| `query` | Query button clicked |
| `add` | Add button clicked |
| `delete` | Delete button clicked |
**Usage**:
```vue
<SearchRow title="User Management" @query="fetchData" @add="openAddDialog">
<template #left>
<NoobButton @click="exportData">Export</NoobButton>
</template>
<NoobInput v-model="searchForm.name" label="Name" />
<NoobSelect v-model="searchForm.status" dict="userStatus" />
</SearchRow>
```
---
### ListTable
Data table with pagination and formatting support.
**File**: `packages/base/data/list-table.vue`
**Props**:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `data` | `any[]/PageResponse` | `[]` | Table data or paginated response |
| `props` | `TableColumn[]` | `[]` | Column definitions |
| `page` | `boolean` | `false` | Enable pagination |
| `height` | `number/string` | `-` | Table height |
| `maxHeight` | `number/string` | `-` | Table max height |
| `example` | `object` | `{}` | Pagination state ({ page, size }) |
| `rowKey` | `string` | `-` | Row key for tree/lazy |
| `selectKey` | `string` | `-` | Key for auto-selection |
| `treeProps` | `object` | `-` | Tree table configuration |
| `lazy` | `boolean` | `false` | Lazy loading |
| `border` | `boolean` | `false` | Show border |
**TableColumn Interface**:
```typescript
interface TableColumn {
code: string; // Property name
name?: string; // Column label (used if i18n not set)
i18n?: string; // i18n key for label
type?: string; // 'selection' | 'index' | 'expand'
width?: number; // Column width
fixed?: string; // 'left' | 'right'
align?: string; // 'left' | 'center' | 'right'
slot?: boolean; // Enable slot rendering
dict?: string; // Dict key for value mapping
timestamp?: boolean; // Format as timestamp
filesize?: boolean; // Format as file size
}
```
**Events**:
| Event | Payload | Description |
|-------|---------|-------------|
| `query` | `void` | Refresh data |
| `selection-change` | `selection[]` | Selection changed |
| `row-click` | `row` | Row clicked |
| `cell-click` | `row, column, cell, event` | Cell clicked |
**Usage**:
```vue
<ListTable
:data="tableData"
:props="columns"
page
:example="pagination"
row-key="id"
@query="fetchData"
@selection-change="onSelect"
>
<template #status="{ row }">
<NoobTag :type="row.active ? 'success' : 'info'">
{{ row.active ? 'Active' : 'Inactive' }}
</NoobTag>
</template>
</ListTable>
<script setup>
const columns = [
{ code: 'id', name: 'ID', width: 80 },
{ code: 'name', name: 'Name' },
{ code: 'status', slot: true },
{ code: 'createdAt', timestamp: true },
{ code: 'size', filesize: true }
];
</script>
```
---
### ListTableDialog
List table with multi-select in a dialog.
**File**: `packages/base/data/listTableDialog.vue`
**Props** (from `ListTableProps`):
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `props` | `TableColumn[]` | `-` | Column definitions |
| `rowKey` | `string` | `-` | Row key for selection |
| `useDicts` | `string[]` | `-` | Dict keys to load |
| `initialPage` | `number` | `1` | Initial page |
| `initialPageSize` | `number` | `-` | Initial page size |
| `initExample` | `object` | `-` | Initial query params |
**Events**:
| Event | Payload | Description |
|-------|---------|-------------|
| `query` | `(handler, example)` | Query handler (async) |
| `confirm` | `(handler, rows)` | Confirm handler (async) |
| `close` | `(handler)` | Close handler (async) |
**Usage**:
```vue
<ListTableDialog
:props="columns"
row-key="id"
:useDicts="['userStatus']"
@query="handleQuery"
@confirm="handleConfirm"
@close="closeDialog"
>
<template #name="{ row }">
<strong>{{ row.name }}</strong>
</template>
</ListTableDialog>
```
---
### ModifyForm
Dynamic form for create/edit operations.
**File**: `packages/base/data/modify-form.vue`
**Props**:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `width` | `number` | `80` | Label width |
| `param` | `object` | `{}` | Form data |
| `rules` | `object` | `{}` | Validation rules |
| `class` | `string` | `''` | Form class |
| `type` | `string` | `null` | Form type |
| `modify` | `boolean` | `false` | Edit mode (hides noModify fields) |
| `items` | `FormItem[]` | `[]` | Form field definitions |
| `confirm` | `string/boolean` | `undefined` | Confirm button config |
| `cancel` | `string/boolean` | `undefined` | Cancel button config |
**FormItem Interface**:
```typescript
interface FormItem {
code: string; // Field key
name?: string; // Label (used if i18n not set)
i18n?: string; // i18n key for label
dict?: string; // Dict for select
maxValue?: number; // Max value for number select
date?: boolean; // Render as date picker
formater?: string; // Date format string
slot?: boolean; // Enable slot rendering
type?: string; // Input type (textarea, etc.)
rows?: number; // Textarea rows
disabled?: boolean; // Disabled state
noModify?: boolean; // Hidden in edit mode
readonly?: boolean; // Readonly state
}
```
**Events**:
| Event | Description |
|-------|-------------|
| `confirm` | Form validated and submitted |
| `cancel` | Cancel clicked |
**Exposed Methods**:
| Method | Description |
|--------|-------------|
| `clearValidate` | Clear validation state |
**Usage**:
```vue
<ModifyForm
ref="formRef"
:param="formData"
:items="formFields"
:rules="formRules"
:modify="isEdit"
@confirm="handleSubmit"
@cancel="handleCancel"
/>
<script setup>
const formFields = [
{ code: 'name', name: 'Username', type: 'input' },
{ code: 'status', dict: 'userStatus' },
{ code: 'birthDate', date: true, formater: 'YYYY-MM-DD' },
{ code: 'remark', type: 'textarea', rows: 3 },
{ code: 'slot_field', slot: true }
];
</script>
```
---
### Descriptions
Key-value display component based on `el-descriptions`.
**File**: `packages/base/data/descriptions.vue`
**Props**:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `data` | `object/array/string` | `0` | Display data |
| `title` | `string` | `null` | Descriptions title |
| `border` | `boolean` | `true` | Show border |
**Usage**:
```vue
<!-- Object key-value -->
<Descriptions :data="{ name: 'John', age: 30 }" title="User Info" />
<!-- Array of {key, value} -->
<Descriptions :data="[{ key: 'Email', value: 'john@example.com' }]" />
<!-- Single value -->
<Descriptions :data="totalCount" title="Statistics" />
```
---
### TableAction
Row action buttons (edit/delete/add) with tooltips.
**File**: `packages/base/data/table-action.vue`
**Props**:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `add` | `boolean` | `false` | Show add button |
| `modify` | `boolean` | `true` | Show edit button |
| `del` | `boolean` | `true` | Show delete button |
**Slots**:
| Slot | Description |
|------|-------------|
| `left` | Extra content before actions |
| `default` | Extra buttons after actions |
**Events**:
| Event | Description |
|-------|-------------|
| `modify` | Edit button clicked |
| `del` | Delete button clicked |
| `add` | Add button clicked |
**Usage**:
```vue
<TableAction @modify="handleEdit" @del="handleDelete" @add="handleAdd">
<template #left>
<NoobButton size="small">Custom</NoobButton>
</template>
</TableAction>
```
---
### Infomation
Card component for displaying single metrics/stats.
**File**: `packages/base/data/infomation.vue`
**Props**:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `num` | `number` | `0` | Numeric value |
| `icon` | `string` | `''` | Element Plus icon name |
| `title` | `string` | `''` | Card title |
| `center` | `boolean` | `false` | Center alignment |
**Usage**:
```vue
<Infomation :num="152" icon="User" title="Total Users" />
<Infomation :num="89.5" icon="TrendCharts" title="Conversion Rate" center />
```
---
## Common Patterns
### Theme Integration
All components use Vuex store for theme values:
```typescript
const { state } = useStore();
// state.style - colors, backgrounds
// state.size - sizes, paddings, fonts
```
Example in SCSS:
```scss
color: v-bind('state.style.color');
background-color: v-bind('state.style.primaryBg');
```
### i18n Integration
Components use `vue3-i18n` for translations:
```typescript
const { t } = useI18n();
// t('key') for translation
```
Common i18n keys:
- `base.add` - Add button
- `base.delete` - Delete button
- `base.select` - Select/Search button
- `base.confirm` - Confirm button
- `base.cancel` - Cancel button
- `rule.pleaseEnter` - Enter placeholder
- `rule.pleaseSelect` - Select placeholder
### v-model Pattern
Components use Vue 3 `defineProps` with `defineEmits`:
```typescript
const prop = defineProps({
modelValue: null,
});
const emit = defineEmits(["update:modelValue"]);
// Sync internal value to prop
watch(myValue, (n) => emit('update:modelValue', n));
```
### Width Management
Item components support multiple width modes:
```typescript
// Fixed width from prop
width.value = prop.width + 'px';
// Or from Vuex store
width.value = state.size.searchWidth;
// Full width
&.full { width: 100%; }
```
### Props Definition Pattern
```typescript
// With defaults
const prop = defineProps({
modelValue: null,
disabled: {
type: Boolean,
default: false,
},
});
// TypeScript interface (preferred for new components)
interface Props {
title: string;
items: TableColumn[];
}
withDefaults(defineProps<Props>(), {
items: () => [],
});
```
---
## Anti-Patterns
1. **Do not access Vuex store directly in templates** - Use computed properties
```vue
<!-- Bad -->
<div>{{ state.style.color }}</div>
<!-- Good -->
<div :style="{ color: themeColor }">text</div>
```
2. **Do not use `any` for typed props** - Define proper interfaces
```typescript
// Bad
prop: any;
// Good
interface TableColumn {
code: string;
name?: string;
}
```
3. **Do not mix Element Plus imports** - Use the component registry
```typescript
// Avoid
import { ElButton } from 'element-plus';
// Prefer (through noob-mengyxu)
import { NoobButton } from 'noob-mengyxu';
```
4. **Avoid prop drilling** - Use provide/inject or Vuex for deep component trees
5. **Do not forget to emit `update:modelValue`** - For v-model to work properly