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>
|