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

571 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
组件开发示例代码
展示项目中组件的编写规范和最佳实践
-->
<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>