1259 lines
23 KiB
Markdown
1259 lines
23 KiB
Markdown
# Uni-App-X 组件库规范 - CRMEB 风格
|
||
|
||
## 📚 概述
|
||
|
||
本文档定义了 mall 项目中可复用组件的开发规范,参考 CRMEB 的组件库设计。所有组件必须遵循此规范。
|
||
|
||
---
|
||
|
||
## 1. 组件分类体系
|
||
|
||
```
|
||
components/
|
||
├── basic/ # 基础组件
|
||
│ ├── Button.uvue
|
||
│ ├── Input.uvue
|
||
│ ├── Select.uvue
|
||
│ ├── Checkbox.uvue
|
||
│ ├── Radio.uvue
|
||
│ └── ...
|
||
├── container/ # 容器组件
|
||
│ ├── Card.uvue
|
||
│ ├── Modal.uvue
|
||
│ ├── Drawer.uvue
|
||
│ └── ...
|
||
├── data/ # 数据展示组件
|
||
│ ├── Table.uvue
|
||
│ ├── List.uvue
|
||
│ ├── Tree.uvue
|
||
│ └── ...
|
||
├── form/ # 表单组件
|
||
│ ├── Form.uvue
|
||
│ ├── FormItem.uvue
|
||
│ ├── DatePicker.uvue
|
||
│ └── ...
|
||
├── feedback/ # 反馈组件
|
||
│ ├── Message.uvue
|
||
│ ├── Toast.uvue
|
||
│ ├── Modal.uvue
|
||
│ └── ...
|
||
└── navigation/ # 导航组件
|
||
├── Tabs.uvue
|
||
├── Breadcrumb.uvue
|
||
└── ...
|
||
```
|
||
|
||
---
|
||
|
||
## 2. 基础组件规范
|
||
|
||
### 2.1 Button(按钮)
|
||
|
||
#### 特性
|
||
|
||
- 4 种类型: primary / default / danger / success
|
||
- 3 种尺寸: sm / md / lg
|
||
- 支持禁用和加载状态
|
||
- 支持前置和后置图标
|
||
|
||
#### 代码示例
|
||
|
||
```uvue
|
||
<template>
|
||
<button
|
||
:class="[
|
||
'btn',
|
||
`btn-${type}`,
|
||
`btn-${size}`,
|
||
{
|
||
'is-loading': loading,
|
||
'is-disabled': disabled,
|
||
}
|
||
]"
|
||
:disabled="disabled || loading"
|
||
@click="handleClick"
|
||
>
|
||
<text v-if="icon && !loading" class="btn-icon-left">{{ icon }}</text>
|
||
<text v-if="loading" class="btn-spinner">⏳</text>
|
||
<text class="btn-text">{{ loading ? '加载中...' : label }}</text>
|
||
</button>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { defineProps, defineEmits } from 'vue'
|
||
|
||
const props = withDefaults(defineProps<{
|
||
type?: 'primary' | 'default' | 'danger' | 'success'
|
||
size?: 'sm' | 'md' | 'lg'
|
||
label: string
|
||
icon?: string
|
||
disabled?: boolean
|
||
loading?: boolean
|
||
}>(), {
|
||
type: 'primary',
|
||
size: 'md',
|
||
disabled: false,
|
||
loading: false,
|
||
})
|
||
|
||
const emit = defineEmits<{
|
||
click: []
|
||
}>()
|
||
|
||
const handleClick = () => {
|
||
if (!props.disabled && !props.loading) {
|
||
emit('click')
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: $space-xs;
|
||
border: none;
|
||
border-radius: $radius-sm;
|
||
cursor: pointer;
|
||
transition: all $transition-duration $transition-timing;
|
||
font-weight: $font-weight-medium;
|
||
white-space: nowrap;
|
||
|
||
// 尺寸变体
|
||
&.btn-sm {
|
||
height: $btn-height-sm;
|
||
font-size: $font-size-sm;
|
||
padding: 0 $space-sm;
|
||
}
|
||
|
||
&.btn-md {
|
||
height: $btn-height;
|
||
font-size: $font-size;
|
||
padding: 0 $space-md;
|
||
}
|
||
|
||
&.btn-lg {
|
||
height: $btn-height-lg;
|
||
font-size: $font-size-md;
|
||
padding: 0 $space-lg;
|
||
}
|
||
|
||
// 类型变体
|
||
&.btn-primary {
|
||
background: $primary-color;
|
||
color: #fff;
|
||
|
||
&:not(.is-disabled):active {
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
&.btn-default {
|
||
background: $background-secondary;
|
||
color: $text-primary;
|
||
border: 1px solid $border-color;
|
||
|
||
&:not(.is-disabled):active {
|
||
background: $border-light;
|
||
}
|
||
}
|
||
|
||
&.btn-danger {
|
||
background: $error-color;
|
||
color: #fff;
|
||
|
||
&:not(.is-disabled):active {
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
&.btn-success {
|
||
background: $success-color;
|
||
color: #fff;
|
||
|
||
&:not(.is-disabled):active {
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
// 状态
|
||
&.is-disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
&.is-loading {
|
||
pointer-events: none;
|
||
}
|
||
|
||
.btn-spinner {
|
||
display: inline-block;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
}
|
||
|
||
@keyframes spin {
|
||
from { transform: rotate(0deg); }
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
.btn-text {
|
||
font-size: inherit;
|
||
}
|
||
|
||
.btn-icon-left {
|
||
margin-right: -$space-xs;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
#### 使用示例
|
||
|
||
```uvue
|
||
<script setup lang="uts">
|
||
const handleCreate = () => {
|
||
console.log('创建')
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<!-- 主按钮 -->
|
||
<Button type="primary" label="创建" @click="handleCreate" />
|
||
|
||
<!-- 小按钮 -->
|
||
<Button type="default" size="sm" label="编辑" />
|
||
|
||
<!-- 加载状态 -->
|
||
<Button type="primary" :loading="loading" label="保存" @click="handleSave" />
|
||
|
||
<!-- 禁用状态 -->
|
||
<Button type="danger" disabled label="删除" />
|
||
</template>
|
||
```
|
||
|
||
### 2.2 Input(输入框)
|
||
|
||
#### 特性
|
||
|
||
- 支持不同类型: text / password / number / email
|
||
- 支持前缀和后缀插槽
|
||
- 支持验证状态反馈
|
||
- 支持 clearable 清空功能
|
||
|
||
#### 代码示例
|
||
|
||
```uvue
|
||
<template>
|
||
<view :class="['input-wrapper', { 'is-error': error }]">
|
||
<view class="input-inner">
|
||
<text v-if="prefix" class="input-prefix">{{ prefix }}</text>
|
||
<input
|
||
:type="type"
|
||
:value="modelValue"
|
||
:placeholder="placeholder"
|
||
:disabled="disabled"
|
||
class="input-field"
|
||
@input="handleInput"
|
||
@focus="handleFocus"
|
||
@blur="handleBlur"
|
||
/>
|
||
<button
|
||
v-if="clearable && modelValue && focused"
|
||
class="input-clear"
|
||
@click="handleClear"
|
||
>
|
||
✕
|
||
</button>
|
||
<text v-if="suffix" class="input-suffix">{{ suffix }}</text>
|
||
</view>
|
||
<text v-if="error" class="input-error">{{ error }}</text>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, defineProps, defineEmits } from 'vue'
|
||
|
||
const props = withDefaults(defineProps<{
|
||
type?: 'text' | 'password' | 'number' | 'email'
|
||
modelValue?: string
|
||
placeholder?: string
|
||
disabled?: boolean
|
||
error?: string
|
||
prefix?: string
|
||
suffix?: string
|
||
clearable?: boolean
|
||
}>(), {
|
||
type: 'text',
|
||
modelValue: '',
|
||
clearable: true,
|
||
})
|
||
|
||
const emit = defineEmits<{
|
||
'update:modelValue': [value: string]
|
||
focus: []
|
||
blur: []
|
||
}>()
|
||
|
||
const focused = ref(false)
|
||
|
||
const handleInput = (event: Event) => {
|
||
const target = event.target as HTMLInputElement
|
||
emit('update:modelValue', target.value)
|
||
}
|
||
|
||
const handleFocus = () => {
|
||
focused.value = true
|
||
emit('focus')
|
||
}
|
||
|
||
const handleBlur = () => {
|
||
focused.value = false
|
||
emit('blur')
|
||
}
|
||
|
||
const handleClear = () => {
|
||
emit('update:modelValue', '')
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.input-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: $space-xs;
|
||
}
|
||
|
||
.input-inner {
|
||
display: flex;
|
||
align-items: center;
|
||
height: $input-height;
|
||
padding: 0 $space-sm;
|
||
border: 1px solid $border-color;
|
||
border-radius: $radius-xs;
|
||
background: $background-primary;
|
||
transition: all $transition-duration $transition-timing;
|
||
|
||
&:focus-within {
|
||
border-color: $primary-color;
|
||
box-shadow: 0 0 0 3px rgba($primary-color, 0.1);
|
||
}
|
||
}
|
||
|
||
.input-wrapper.is-error .input-inner {
|
||
border-color: $error-color;
|
||
box-shadow: 0 0 0 3px rgba($error-color, 0.1);
|
||
}
|
||
|
||
.input-prefix {
|
||
color: $text-tertiary;
|
||
margin-right: $space-sm;
|
||
flex: none;
|
||
}
|
||
|
||
.input-field {
|
||
flex: 1;
|
||
height: 100%;
|
||
border: none;
|
||
outline: none;
|
||
font-size: $font-size;
|
||
color: $text-primary;
|
||
background: transparent;
|
||
|
||
&::placeholder {
|
||
color: $text-tertiary;
|
||
}
|
||
|
||
&:disabled {
|
||
color: $text-disabled;
|
||
cursor: not-allowed;
|
||
}
|
||
}
|
||
|
||
.input-clear {
|
||
background: none;
|
||
border: none;
|
||
color: $text-tertiary;
|
||
padding: $space-xs;
|
||
cursor: pointer;
|
||
margin: 0 $space-xs;
|
||
|
||
&:hover {
|
||
color: $text-primary;
|
||
}
|
||
}
|
||
|
||
.input-suffix {
|
||
color: $text-tertiary;
|
||
margin-left: $space-sm;
|
||
flex: none;
|
||
}
|
||
|
||
.input-error {
|
||
font-size: $font-size-xs;
|
||
color: $error-color;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
### 2.3 Select(下拉选择)
|
||
|
||
#### 特性
|
||
|
||
- 支持单选和多选
|
||
- 支持搜索过滤
|
||
- 支持虚拟滚动(大数据)
|
||
- 支持自定义选项模板
|
||
|
||
#### 示例
|
||
|
||
```uvue
|
||
<template>
|
||
<view class="select-wrapper">
|
||
<view
|
||
class="select-trigger"
|
||
@click="toggleDropdown"
|
||
>
|
||
<text class="select-value">
|
||
{{ selectedLabel || placeholder }}
|
||
</text>
|
||
<text class="select-arrow">▼</text>
|
||
</view>
|
||
|
||
<view v-if="visible" class="select-dropdown">
|
||
<input
|
||
v-if="searchable"
|
||
class="select-search"
|
||
placeholder="搜索..."
|
||
@input="handleSearch"
|
||
/>
|
||
<view class="select-options">
|
||
<view
|
||
v-for="option in filteredOptions"
|
||
:key="option.value"
|
||
class="select-option"
|
||
:class="{ active: modelValue === option.value }"
|
||
@click="handleSelect(option)"
|
||
>
|
||
{{ option.label }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, computed, defineProps, defineEmits } from 'vue'
|
||
|
||
const props = withDefaults(defineProps<{
|
||
options: Array<{ label: string; value: string }>
|
||
modelValue?: string
|
||
placeholder?: string
|
||
searchable?: boolean
|
||
}>(), {
|
||
searchable: false,
|
||
})
|
||
|
||
const emit = defineEmits<{
|
||
'update:modelValue': [value: string]
|
||
}>()
|
||
|
||
const visible = ref(false)
|
||
const searchQuery = ref('')
|
||
|
||
const selectedLabel = computed(() => {
|
||
return props.options.find(opt => opt.value === props.modelValue)?.label
|
||
})
|
||
|
||
const filteredOptions = computed(() => {
|
||
if (!searchQuery.value) {
|
||
return props.options
|
||
}
|
||
return props.options.filter(opt =>
|
||
opt.label.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||
)
|
||
})
|
||
|
||
const toggleDropdown = () => {
|
||
visible.value = !visible.value
|
||
}
|
||
|
||
const handleSelect = (option: any) => {
|
||
emit('update:modelValue', option.value)
|
||
visible.value = false
|
||
}
|
||
|
||
const handleSearch = (event: Event) => {
|
||
const target = event.target as HTMLInputElement
|
||
searchQuery.value = target.value
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.select-wrapper {
|
||
position: relative;
|
||
}
|
||
|
||
.select-trigger {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: $input-height;
|
||
padding: 0 $space-sm;
|
||
border: 1px solid $border-color;
|
||
border-radius: $radius-xs;
|
||
background: $background-primary;
|
||
cursor: pointer;
|
||
transition: all $transition-duration $transition-timing;
|
||
|
||
&:hover {
|
||
border-color: $primary-color;
|
||
}
|
||
}
|
||
|
||
.select-value {
|
||
color: $text-primary;
|
||
font-size: $font-size;
|
||
}
|
||
|
||
.select-arrow {
|
||
color: $text-tertiary;
|
||
margin-left: $space-sm;
|
||
transition: transform $transition-duration $transition-timing;
|
||
}
|
||
|
||
.select-wrapper.active .select-arrow {
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
.select-dropdown {
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 0;
|
||
right: 0;
|
||
margin-top: $space-xs;
|
||
background: $background-primary;
|
||
border: 1px solid $border-color;
|
||
border-radius: $radius-xs;
|
||
box-shadow: $shadow-lg;
|
||
z-index: $z-index-dropdown;
|
||
}
|
||
|
||
.select-search {
|
||
width: 100%;
|
||
height: $input-height-sm;
|
||
padding: 0 $space-sm;
|
||
border: none;
|
||
border-bottom: 1px solid $border-light;
|
||
font-size: $font-size;
|
||
}
|
||
|
||
.select-options {
|
||
max-height: 250px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.select-option {
|
||
padding: $space-sm $space-md;
|
||
cursor: pointer;
|
||
transition: background-color $transition-duration $transition-timing;
|
||
color: $text-primary;
|
||
font-size: $font-size;
|
||
|
||
&:hover {
|
||
background-color: $background-secondary;
|
||
}
|
||
|
||
&.active {
|
||
background-color: rgba($primary-color, 0.1);
|
||
color: $primary-color;
|
||
font-weight: $font-weight-medium;
|
||
}
|
||
}
|
||
</style>
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 容器组件规范
|
||
|
||
### 3.1 Card(卡片)
|
||
|
||
```uvue
|
||
<template>
|
||
<view :class="['card', shadowLevel]">
|
||
<view v-if="$slots.header" class="card-header">
|
||
<slot name="header" />
|
||
</view>
|
||
<view class="card-body">
|
||
<slot />
|
||
</view>
|
||
<view v-if="$slots.footer" class="card-footer">
|
||
<slot name="footer" />
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { defineProps } from 'vue'
|
||
|
||
const props = withDefaults(defineProps<{
|
||
shadowLevel?: 'sm' | 'md' | 'lg'
|
||
}>(), {
|
||
shadowLevel: 'sm',
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.card {
|
||
background: $background-primary;
|
||
border-radius: $radius;
|
||
overflow: hidden;
|
||
|
||
&.sm {
|
||
box-shadow: $shadow-sm;
|
||
}
|
||
|
||
&.md {
|
||
box-shadow: $shadow;
|
||
}
|
||
|
||
&.lg {
|
||
box-shadow: $shadow-lg;
|
||
}
|
||
}
|
||
|
||
.card-header {
|
||
padding: $space-md;
|
||
border-bottom: 1px solid $border-light;
|
||
background: $background-secondary;
|
||
}
|
||
|
||
.card-body {
|
||
padding: $space-md;
|
||
}
|
||
|
||
.card-footer {
|
||
padding: $space-md;
|
||
border-top: 1px solid $border-light;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
### 3.2 Modal(模态框)
|
||
|
||
```uvue
|
||
<template>
|
||
<view v-if="visible" class="modal-overlay" @click="handleBackdropClick">
|
||
<view class="modal-dialog" @click="stopPropagation">
|
||
<view class="modal-header">
|
||
<text class="modal-title">{{ title }}</text>
|
||
<button class="modal-close" @click="handleClose">✕</button>
|
||
</view>
|
||
<view class="modal-body">
|
||
<slot />
|
||
</view>
|
||
<view class="modal-footer">
|
||
<button class="btn btn-default" @click="handleCancel">取消</button>
|
||
<button class="btn btn-primary" @click="handleConfirm">确认</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { defineProps, defineEmits } from 'vue'
|
||
|
||
const props = defineProps<{
|
||
visible: boolean
|
||
title: string
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
close: []
|
||
confirm: []
|
||
cancel: []
|
||
}>()
|
||
|
||
const handleBackdropClick = () => {
|
||
emit('close')
|
||
}
|
||
|
||
const handleClose = () => {
|
||
emit('close')
|
||
}
|
||
|
||
const handleCancel = () => {
|
||
emit('cancel')
|
||
}
|
||
|
||
const handleConfirm = () => {
|
||
emit('confirm')
|
||
}
|
||
|
||
const stopPropagation = (event: Event) => {
|
||
event.stopPropagation()
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.45);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: $z-index-modal-backdrop;
|
||
}
|
||
|
||
.modal-dialog {
|
||
background: $background-primary;
|
||
border-radius: $radius;
|
||
width: 90%;
|
||
max-width: 500px;
|
||
box-shadow: $shadow-xl;
|
||
z-index: $z-index-modal;
|
||
}
|
||
|
||
.modal-header {
|
||
padding: $space-md;
|
||
border-bottom: 1px solid $border-light;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: $font-size-md;
|
||
font-weight: $font-weight-semibold;
|
||
color: $text-primary;
|
||
}
|
||
|
||
.modal-close {
|
||
background: none;
|
||
border: none;
|
||
color: $text-tertiary;
|
||
padding: 0;
|
||
cursor: pointer;
|
||
font-size: $font-size-lg;
|
||
|
||
&:hover {
|
||
color: $text-primary;
|
||
}
|
||
}
|
||
|
||
.modal-body {
|
||
padding: $space-md;
|
||
max-height: 60vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-footer {
|
||
padding: $space-md;
|
||
border-top: 1px solid $border-light;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: $space-sm;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 表单组件规范
|
||
|
||
### 4.1 Form(表单容器)
|
||
|
||
```uvue
|
||
<template>
|
||
<form class="form" @submit.prevent="handleSubmit">
|
||
<slot />
|
||
</form>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { defineEmits } from 'vue'
|
||
|
||
const emit = defineEmits<{
|
||
submit: [formData: Record<string, any>]
|
||
}>()
|
||
|
||
const handleSubmit = () => {
|
||
// 实现表单验证逻辑
|
||
emit('submit', {})
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.form {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: $space-lg;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
### 4.2 FormItem(表单项)
|
||
|
||
```uvue
|
||
<template>
|
||
<view class="form-item">
|
||
<label class="form-label">
|
||
<text v-if="required" class="required-mark">*</text>
|
||
{{ label }}
|
||
</label>
|
||
<slot />
|
||
<text v-if="error" class="form-error">{{ error }}</text>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { defineProps } from 'vue'
|
||
|
||
const props = withDefaults(defineProps<{
|
||
label: string
|
||
required?: boolean
|
||
error?: string
|
||
}>(), {
|
||
required: false,
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.form-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: $space-sm;
|
||
}
|
||
|
||
.form-label {
|
||
font-size: $font-size-sm;
|
||
color: $text-primary;
|
||
font-weight: $font-weight-medium;
|
||
}
|
||
|
||
.required-mark {
|
||
color: $error-color;
|
||
margin-right: $space-xs;
|
||
}
|
||
|
||
.form-error {
|
||
font-size: $font-size-xs;
|
||
color: $error-color;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 数据展示组件规范
|
||
|
||
### 5.1 Table(表格)
|
||
|
||
核心要点:
|
||
|
||
- 支持排序和筛选
|
||
- 支持行选择
|
||
- 支持固定列头
|
||
- 支持虚拟滚动
|
||
|
||
```uvue
|
||
<template>
|
||
<view class="table-wrapper">
|
||
<view class="table-header">
|
||
<view class="table-row">
|
||
<view v-for="col in columns" :key="col.key" class="table-cell" :style="{ width: col.width }">
|
||
<text class="table-header-text">{{ col.title }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="table-body">
|
||
<view v-for="row in data" :key="row.id" class="table-row">
|
||
<view v-for="col in columns" :key="col.key" class="table-cell" :style="{ width: col.width }">
|
||
<slot :name="`cell-${col.key}`" :row="row">
|
||
{{ row[col.key] }}
|
||
</slot>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { defineProps } from 'vue'
|
||
|
||
const props = defineProps<{
|
||
columns: Array<{
|
||
key: string
|
||
title: string
|
||
width?: string
|
||
}>
|
||
data: Array<Record<string, any>>
|
||
}>()
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.table-wrapper {
|
||
border: 1px solid $border-light;
|
||
border-radius: $radius;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.table-header {
|
||
background: $background-secondary;
|
||
border-bottom: 1px solid $border-light;
|
||
}
|
||
|
||
.table-row {
|
||
display: flex;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.table-cell {
|
||
padding: $space-md;
|
||
border-right: 1px solid $border-light;
|
||
flex: 1;
|
||
|
||
&:last-child {
|
||
border-right: none;
|
||
}
|
||
}
|
||
|
||
.table-header-text {
|
||
font-size: $font-size-sm;
|
||
font-weight: $font-weight-semibold;
|
||
color: $text-primary;
|
||
}
|
||
|
||
.table-body {
|
||
.table-row {
|
||
border-bottom: 1px solid $border-light;
|
||
|
||
&:hover {
|
||
background: $background-secondary;
|
||
}
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
}
|
||
|
||
.table-cell {
|
||
color: $text-primary;
|
||
font-size: $font-size;
|
||
}
|
||
}
|
||
</style>
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 反馈组件规范
|
||
|
||
### 6.1 Message(消息提示)
|
||
|
||
```uvue
|
||
<script setup lang="uts">
|
||
import { ref, computed } from 'vue'
|
||
|
||
export interface MessageOptions {
|
||
type: 'info' | 'success' | 'warning' | 'error'
|
||
content: string
|
||
duration?: number
|
||
}
|
||
|
||
const messages = ref<Array<MessageOptions & { id: string }>>([])
|
||
|
||
const show = (options: MessageOptions) => {
|
||
const id = Date.now().toString()
|
||
const message = { ...options, id }
|
||
messages.value.push(message)
|
||
|
||
if (options.duration !== 0) {
|
||
setTimeout(() => {
|
||
messages.value = messages.value.filter(m => m.id !== id)
|
||
}, options.duration || 3000)
|
||
}
|
||
|
||
return {
|
||
close: () => {
|
||
messages.value = messages.value.filter(m => m.id !== id)
|
||
},
|
||
}
|
||
}
|
||
|
||
// 导出方法供全局使用
|
||
defineExpose({
|
||
show,
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<view class="messages">
|
||
<view
|
||
v-for="msg in messages"
|
||
:key="msg.id"
|
||
:class="['message', `message-${msg.type}`]"
|
||
>
|
||
{{ msg.content }}
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.messages {
|
||
position: fixed;
|
||
top: $space-lg;
|
||
right: $space-lg;
|
||
z-index: $z-index-notification;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: $space-md;
|
||
}
|
||
|
||
.message {
|
||
padding: $space-md;
|
||
border-radius: $radius;
|
||
color: #fff;
|
||
font-size: $font-size-sm;
|
||
box-shadow: $shadow-lg;
|
||
animation: slideIn 0.3s ease-out;
|
||
|
||
&.message-info {
|
||
background: $primary-color;
|
||
}
|
||
|
||
&.message-success {
|
||
background: $success-color;
|
||
}
|
||
|
||
&.message-warning {
|
||
background: $warning-color;
|
||
}
|
||
|
||
&.message-error {
|
||
background: $error-color;
|
||
}
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateX(100%);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateX(0);
|
||
}
|
||
}
|
||
</style>
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 组件命名规范
|
||
|
||
### 7.1 文件命名
|
||
|
||
- 使用 **PascalCase**(帕斯卡命名法)
|
||
- 例如: `Button.uvue`, `FormItem.uvue`, `TableCell.uvue`
|
||
|
||
### 7.2 Props 命名
|
||
|
||
- 使用 **camelCase**(驼峰命名法)
|
||
- 例如: `modelValue`, `isLoading`, `onConfirm`
|
||
|
||
### 7.3 事件命名
|
||
|
||
- 使用 **camelCase**(驼峰命名法)
|
||
- 例如: `@click`, `@change`, `@submit`
|
||
|
||
### 7.4 Slot 命名
|
||
|
||
- 使用 **kebab-case**(短横线命名法)
|
||
- 例如: `<slot name="header" />`, `<slot name="footer" />`
|
||
|
||
### 7.5 Class 命名
|
||
|
||
- 使用 **kebab-case**(短横线命名法)
|
||
- 例如: `.form-item`, `.btn-primary`, `.list-item`
|
||
|
||
---
|
||
|
||
## 8. Props 和 Emit 规范
|
||
|
||
### 8.1 Props 验证
|
||
|
||
```typescript
|
||
interface ButtonProps {
|
||
type?: "primary" | "default" | "danger" | "success";
|
||
size?: "sm" | "md" | "lg";
|
||
label: string;
|
||
disabled?: boolean;
|
||
loading?: boolean;
|
||
}
|
||
|
||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||
type: "primary",
|
||
size: "md",
|
||
disabled: false,
|
||
loading: false,
|
||
});
|
||
```
|
||
|
||
### 8.2 Emit 验证
|
||
|
||
```typescript
|
||
const emit = defineEmits<{
|
||
click: [];
|
||
change: [value: string];
|
||
submit: [formData: Record<string, any>];
|
||
}>();
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 组件文档模板
|
||
|
||
每个组件都应包含以下文档:
|
||
|
||
```markdown
|
||
# Button 组件
|
||
|
||
## 描述
|
||
|
||
按钮组件,支持多种类型和尺寸。
|
||
|
||
## Props
|
||
|
||
| 属性 | 类型 | 默认值 | 说明 |
|
||
| -------- | ------- | ------- | ---------------------------------------- |
|
||
| type | string | primary | 按钮类型: primary/default/danger/success |
|
||
| size | string | md | 按钮尺寸: sm/md/lg |
|
||
| label | string | - | 按钮文本 |
|
||
| disabled | boolean | false | 是否禁用 |
|
||
| loading | boolean | false | 是否加载中 |
|
||
|
||
## Events
|
||
|
||
| 事件 | 参数 | 说明 |
|
||
| ----- | ---- | -------- |
|
||
| click | - | 点击事件 |
|
||
|
||
## Slots
|
||
|
||
无
|
||
|
||
## 示例
|
||
|
||
\`\`\`vue
|
||
<Button type="primary" label="创建" @click="handleCreate" />
|
||
\`\`\`
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 组件开发清单
|
||
|
||
在开发新组件前,确保已完成以下事项:
|
||
|
||
- [ ] 组件放在正确的分类目录
|
||
- [ ] 组件名称遵循 PascalCase
|
||
- [ ] Props 已定义 TypeScript 接口
|
||
- [ ] Emit 事件已定义类型
|
||
- [ ] 样式变量全部来自 uni.scss
|
||
- [ ] 支持响应式设计
|
||
- [ ] 提供了完整的使用示例
|
||
- [ ] 编写了 JSDoc 文档
|
||
- [ ] 测试通过(所有状态)
|
||
- [ ] 无 console.log 和调试代码
|
||
|
||
---
|
||
|
||
## 11. 常见模式
|
||
|
||
### 11.1 v-model 支持
|
||
|
||
```typescript
|
||
// Props
|
||
const props = defineProps<{
|
||
modelValue?: string;
|
||
}>();
|
||
|
||
// Emit
|
||
const emit = defineEmits<{
|
||
"update:modelValue": [value: string];
|
||
}>();
|
||
|
||
// 使用
|
||
const handleChange = (value: string) => {
|
||
emit("update:modelValue", value);
|
||
};
|
||
```
|
||
|
||
### 11.2 插槽使用
|
||
|
||
```uvue
|
||
<template>
|
||
<view class="component">
|
||
<slot name="header" />
|
||
<slot /> <!-- 默认插槽 -->
|
||
<slot name="footer" />
|
||
</view>
|
||
</template>
|
||
|
||
<!-- 使用 -->
|
||
<Component>
|
||
<template #header>
|
||
<text>头部</text>
|
||
</template>
|
||
<text>内容</text>
|
||
<template #footer>
|
||
<text>底部</text>
|
||
</template>
|
||
</Component>
|
||
```
|
||
|
||
### 11.3 条件渲染
|
||
|
||
```uvue
|
||
<view v-if="visible" class="component">
|
||
<!-- 内容 -->
|
||
</view>
|
||
|
||
<view v-show="visible" class="component">
|
||
<!-- 使用 v-show 用于频繁显隐,性能更好 -->
|
||
</view>
|
||
```
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
✅ **组件开发核心原则**:
|
||
|
||
1. **单一职责** - 每个组件只做一件事
|
||
2. **可复用性** - 组件应该能在多个场景使用
|
||
3. **灵活性** - 提供足够的 Props 和 Slots 来自定义
|
||
4. **可维护性** - 清晰的代码结构和充分的文档
|
||
5. **性能** - 避免不必要的渲染和内存泄漏
|
||
|
||
❌ **禁止做法**:
|
||
|
||
- 在组件中混合业务逻辑
|
||
- 创建过度抽象的组件
|
||
- 使用硬编码的样式值
|
||
- 忽视响应式设计
|
||
- 没有文档的组件
|
||
|
||
---
|
||
|
||
**文档版本**: 1.0
|
||
**最后更新**: 2026-01-31
|
||
**维护者**: AI Assistant
|