shop-wx/doc/examples/component-example.vue

571 lines
10 KiB
Vue
Raw Normal View History

<!--
组件开发示例代码
展示项目中组件的编写规范和最佳实践
-->
<script lang="ts" setup>
/**
* 组件Props接口定义
*/
interface Props {
/** 标题文本 */
title: string;
/** 是否显示(用于 v-model */
modelValue: boolean;
/** 大小尺寸 */
size?: 'large' | 'medium' | 'small';
/** 用户信息 */
userInfo?: {
id: number;
name: string;
avatar?: string;
} | null;
/** 是否显示操作按钮 */
showActions?: boolean;
/** 自定义类名 */
customClass?: string;
}
/**
* 组件Emits接口定义
*/
interface Emits {
/** 更新显示状态 */
(e: 'update:modelValue', value: boolean): void;
/** 点击确认按钮 */
(e: 'confirm', data: any): void;
/** 点击取消按钮 */
(e: 'cancel'): void;
/** 点击删除按钮 */
(e: 'delete', id: number): void;
}
/**
* Props默认值设置
* 使用 withDefaults 提供默认值
*/
const props = withDefaults(defineProps<Props>(), {
size: 'medium',
userInfo: null,
showActions: true,
customClass: '',
});
/**
* Emits定义
* 使用 defineEmits 定义组件事件
*/
const emit = defineEmits<Emits>();
/**
* 组件内部状态
*/
// 内部数据
const internalData = ref({
inputValue: '',
selectedValue: '',
});
// 是否禁用
const disabled = ref(false);
/**
* 计算属性
*/
// 根据 size 计算类名
const sizeClass = computed(() => {
const sizeMap = {
large: 'size-large',
medium: 'size-medium',
small: 'size-small',
};
return sizeMap[props.size];
});
// 组件类名
const componentClass = computed(() => {
return ['user-card', props.customClass, sizeClass.value].filter(Boolean);
});
/**
* 方法定义
*/
// ✅ 关闭弹窗
/**
* 关闭弹窗
*/
const handleClose = () => {
emit('update:modelValue', false);
};
// ✅ 点击遮罩关闭
/**
* 点击遮罩关闭弹窗
* @param e 事件对象
*/
const handleMaskClick = (e: MouseEvent) => {
// 阻止事件冒泡
e.stopPropagation();
handleClose();
};
// ✅ 阻止内容区域点击
/**
* 阻止内容区域点击事件冒泡
* @param e 事件对象
*/
const handleContentClick = (e: MouseEvent) => {
e.stopPropagation();
};
// ✅ 确认操作
/**
* 点击确认按钮
*/
const handleConfirm = () => {
const result = {
inputValue: internalData.value.inputValue,
selectedValue: internalData.value.selectedValue,
userInfo: props.userInfo,
};
emit('confirm', result);
};
// ✅ 取消操作
/**
* 点击取消按钮
*/
const handleCancel = () => {
emit('cancel');
handleClose();
};
// ✅ 删除用户
/**
* 点击删除按钮
* @param id 用户ID
*/
const handleDelete = (id: number) => {
uni.showModal({
title: '确认删除',
content: '确定要删除该用户吗?',
success: (res) => {
if (res.confirm) {
emit('delete', id);
}
},
});
};
// ✅ 输入处理
/**
* 处理输入框输入
* @param value 输入值
*/
const handleInput = (value: string) => {
internalData.value.inputValue = value;
};
// ✅ 选择处理
/**
* 处理选择器变化
* @param value 选中值
*/
const handleSelectChange = (value: string) => {
internalData.value.selectedValue = value;
};
// ✅ 获取用户头像
/**
* 获取用户头像地址
* @returns 头像地址
*/
const getUserAvatar = () => {
if (props.userInfo?.avatar) {
return props.userInfo.avatar;
}
return '/static/default-avatar.png';
};
// ✅ 格式化用户信息
/**
* 格式化显示用户信息
* @returns 用户信息字符串
*/
const formatUserInfo = () => {
if (!props.userInfo) {
return '未选择用户';
}
return `${props.userInfo.name} (#${props.userInfo.id})`;
};
/**
* Watch监听
*/
// 监听 modelValue 变化,重置表单
watch(
() => props.modelValue,
(newValue) => {
if (newValue) {
// 弹窗打开时重置表单
internalData.value.inputValue = '';
internalData.value.selectedValue = '';
}
}
);
</script>
<template>
<!-- 弹窗容器 -->
<view
v-if="modelValue"
class="user-card-mask"
@tap="handleMaskClick"
>
<view
:class="componentClass"
class="user-card"
@tap="handleContentClick"
>
<!-- 头部标题 -->
<view class="user-card__header">
<view class="user-card__title">
<text class="title-text">{{ title }}</text>
</view>
<view class="user-card__close" @tap="handleClose">
<text class="close-icon">×</text>
</view>
</view>
<!-- 用户信息展示 -->
<view class="user-card__content">
<view class="user-info">
<image
class="user-avatar"
:src="getUserAvatar()"
mode="aspectFill"
/>
<view class="user-details">
<text class="user-name">{{ formatUserInfo() }}</text>
<text class="user-id" v-if="userInfo">
ID: {{ userInfo.id }}
</text>
</view>
</view>
<!-- 表单输入区域 -->
<view class="form-area">
<!-- 输入框 -->
<view class="form-item">
<label class="form-label">输入内容</label>
<input
class="form-input"
v-model="internalData.inputValue"
placeholder="请输入内容"
@input="handleInput"
/>
</view>
<!-- 选择器 -->
<view class="form-item">
<label class="form-label">选择类型</label>
<picker
class="form-picker"
@change="handleSelectChange"
:range="['选项1', '选项2', '选项3']"
>
<view class="picker-display">
{{ internalData.selectedValue || '请选择' }}
</view>
</picker>
</view>
<!-- 禁用状态示例 -->
<view class="form-item">
<label class="form-label">禁用状态</label>
<view
class="disabled-input"
:class="{ 'is-disabled': disabled }"
>
{{ disabled ? '已禁用' : '正常状态' }}
</view>
</view>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="user-card__footer" v-if="showActions">
<view class="action-buttons">
<button
class="btn btn-cancel"
@tap="handleCancel"
>
取消
</button>
<button
class="btn btn-confirm"
@tap="handleConfirm"
>
确认
</button>
<button
class="btn btn-danger"
v-if="userInfo?.id"
@tap="handleDelete(userInfo.id)"
>
删除
</button>
</view>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
/* 遮罩层 */
.user-card-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
padding: 40rpx;
}
/* 卡片主体 */
.user-card {
background: white;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.1);
max-width: 600rpx;
width: 100%;
max-height: 90vh;
overflow-y: auto;
/* 不同尺寸样式 */
&.size-large {
.user-card__header {
padding: 40rpx;
}
.user-card__content {
padding: 40rpx;
}
.user-card__footer {
padding: 40rpx;
}
}
&.size-medium {
.user-card__header {
padding: 32rpx;
}
.user-card__content {
padding: 32rpx;
}
.user-card__footer {
padding: 32rpx;
}
}
&.size-small {
.user-card__header {
padding: 24rpx;
}
.user-card__content {
padding: 24rpx;
}
.user-card__footer {
padding: 24rpx;
}
}
}
/* 头部 */
.user-card__header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1rpx solid #f0f0f0;
background: linear-gradient(90deg, #f0f8ff, #e6f2ff);
.user-card__title {
flex: 1;
.title-text {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
}
.user-card__close {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(0, 0, 0, 0.05);
transition: all 0.3s;
&:active {
background: rgba(0, 0, 0, 0.1);
transform: scale(0.95);
}
.close-icon {
font-size: 40rpx;
color: #666;
line-height: 1;
}
}
}
/* 内容区 */
.user-card__content {
background: white;
}
/* 用户信息 */
.user-info {
display: flex;
align-items: center;
gap: 20rpx;
padding: 20rpx;
background: #f9f9f9;
border-radius: 8rpx;
margin-bottom: 20rpx;
.user-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
border: 2rpx solid #e0e0e0;
}
.user-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
.user-name {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.user-id {
font-size: 24rpx;
color: #999;
}
}
}
/* 表单区域 */
.form-area {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.form-item {
display: flex;
align-items: center;
gap: 20rpx;
.form-label {
min-width: 120rpx;
font-size: 28rpx;
color: #666;
}
.form-input {
flex: 1;
padding: 16rpx 20rpx;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 28rpx;
}
.form-picker {
flex: 1;
.picker-display {
padding: 16rpx 20rpx;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 28rpx;
}
}
.disabled-input {
flex: 1;
padding: 16rpx 20rpx;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 28rpx;
color: #999;
&.is-disabled {
opacity: 0.6;
}
}
}
/* 底部 */
.user-card__footer {
background: white;
border-top: 1rpx solid #f0f0f0;
}
.action-buttons {
display: flex;
gap: 20rpx;
justify-content: flex-end;
.btn {
padding: 20rpx 40rpx;
border-radius: 8rpx;
font-size: 28rpx;
border: none;
transition: all 0.3s;
&:active {
transform: scale(0.95);
}
&.btn-cancel {
background: #f5f5f5;
color: #666;
}
&.btn-confirm {
background: #4a90ff;
color: white;
}
&.btn-danger {
background: #f56c6c;
color: white;
}
}
}
</style>