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