354 lines
7.9 KiB
Markdown
354 lines
7.9 KiB
Markdown
# 实现竖屏移动端卡片展示组件
|
||
|
||
## 组件设计目标
|
||
- 适配竖屏移动端的卡片展示组件
|
||
- 包含图片展示、加载蒙层、预览功能
|
||
- 底部功能按钮(修改、编辑、场景图、下载、删除)
|
||
- 向父组件抛出事件
|
||
|
||
## 实现步骤
|
||
|
||
### 1. 实现 `shu.vue` 组件
|
||
- 设计组件结构:主容器、图片区域、功能按钮区域
|
||
- 实现响应式布局,适配竖屏移动端
|
||
- 使用 Scoped CSS + CSS 变量实现样式隔离与主题定制
|
||
|
||
### 2. 图片展示与加载效果
|
||
- 使用 `el-image` 组件实现图片展示
|
||
- 添加加载状态管理,显示加载蒙层
|
||
- 实现图片加载完成后的过渡效果
|
||
|
||
### 3. 图片预览功能
|
||
- 集成图片预览功能,适配 H5 移动端
|
||
- 点击图片触发预览,支持手势操作
|
||
|
||
### 4. 底部功能按钮
|
||
- 实现修改、编辑、场景图、下载、删除按钮
|
||
- 每个按钮向父组件抛出对应的事件
|
||
- 按钮样式设计符合 UI/UX 要求
|
||
|
||
### 5. 与父组件交互
|
||
- 在 `CreateProjectShu.vue` 中引入并使用 `shu.vue` 组件
|
||
- 实现事件处理逻辑,接收并处理子组件抛出的事件
|
||
|
||
## 代码实现
|
||
|
||
### `shu.vue` 组件
|
||
```vue
|
||
<template>
|
||
<div class="shu-card-container">
|
||
<!-- 图片展示区域 -->
|
||
<div class="image-wrapper" @click="handlePreview">
|
||
<el-image
|
||
:src="props.imageUrl"
|
||
:fit="'cover'"
|
||
:preview-src-list="[props.imageUrl]"
|
||
@load="handleImageLoad"
|
||
@error="handleImageError"
|
||
>
|
||
<!-- 加载蒙层 -->
|
||
<template #loading>
|
||
<div class="loading-mask">
|
||
<div class="loading-spinner"></div>
|
||
<div class="loading-text">图片加载中...</div>
|
||
</div>
|
||
</template>
|
||
</el-image>
|
||
</div>
|
||
|
||
<!-- 功能按钮区域 -->
|
||
<div class="action-buttons">
|
||
<button class="action-btn" @click="handleModify" title="修改">
|
||
<el-icon class="btn-icon"><ChatDotRound /></el-icon>
|
||
<span class="btn-text">修改</span>
|
||
</button>
|
||
<button class="action-btn" @click="handleEdit" title="编辑">
|
||
<el-icon class="btn-icon"><EditPen /></el-icon>
|
||
<span class="btn-text">编辑</span>
|
||
</button>
|
||
<button class="action-btn" @click="handleScene" title="场景图">
|
||
<el-icon class="btn-icon"><Grid /></el-icon>
|
||
<span class="btn-text">场景图</span>
|
||
</button>
|
||
<button class="action-btn" @click="handleDownload" title="下载">
|
||
<el-icon class="btn-icon"><Download /></el-icon>
|
||
<span class="btn-text">下载</span>
|
||
</button>
|
||
<button class="action-btn delete-btn" @click="handleDelete" title="删除">
|
||
<el-icon class="btn-icon"><Delete /></el-icon>
|
||
<span class="btn-text">删除</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from 'vue';
|
||
import { ElImage, ElIcon } from 'element-plus';
|
||
import { ChatDotRound, EditPen, Grid, Download, Delete } from '@element-plus/icons-vue';
|
||
|
||
// 定义组件属性
|
||
const props = defineProps({
|
||
// 图片URL
|
||
imageUrl: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
// 卡片数据
|
||
cardData: {
|
||
type: Object,
|
||
default: () => ({})
|
||
}
|
||
});
|
||
|
||
// 定义事件
|
||
const emit = defineEmits(['preview', 'modify', 'edit', 'scene', 'download', 'delete']);
|
||
|
||
// 图片加载状态
|
||
const isLoading = ref(true);
|
||
|
||
// 处理图片加载完成
|
||
const handleImageLoad = () => {
|
||
isLoading.value = false;
|
||
};
|
||
|
||
// 处理图片加载错误
|
||
const handleImageError = () => {
|
||
isLoading.value = false;
|
||
};
|
||
|
||
// 处理图片预览
|
||
const handlePreview = () => {
|
||
emit('preview', props.imageUrl);
|
||
};
|
||
|
||
// 处理修改按钮点击
|
||
const handleModify = (e) => {
|
||
e.stopPropagation();
|
||
emit('modify', props.cardData);
|
||
};
|
||
|
||
// 处理编辑按钮点击
|
||
const handleEdit = (e) => {
|
||
e.stopPropagation();
|
||
emit('edit', props.cardData);
|
||
};
|
||
|
||
// 处理场景图按钮点击
|
||
const handleScene = (e) => {
|
||
e.stopPropagation();
|
||
emit('scene', props.cardData);
|
||
};
|
||
|
||
// 处理下载按钮点击
|
||
const handleDownload = (e) => {
|
||
e.stopPropagation();
|
||
emit('download', props.imageUrl);
|
||
};
|
||
|
||
// 处理删除按钮点击
|
||
const handleDelete = (e) => {
|
||
e.stopPropagation();
|
||
emit('delete', props.cardData);
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 组件样式 */
|
||
.shu-card-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: 100%;
|
||
max-width: 400px;
|
||
margin: 0 auto;
|
||
background: #ffffff;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
/* 图片区域样式 */
|
||
.image-wrapper {
|
||
position: relative;
|
||
width: 100%;
|
||
padding-bottom: 150%; /* 2:3 竖屏比例 */
|
||
overflow: hidden;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.image-wrapper :deep(.el-image) {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.image-wrapper :deep(.el-image__inner) {
|
||
object-fit: cover;
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.image-wrapper:hover :deep(.el-image__inner) {
|
||
transform: scale(1.02);
|
||
}
|
||
|
||
/* 加载蒙层样式 */
|
||
.loading-mask {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.9) 0%, rgba(248, 249, 250, 0.95) 100%);
|
||
transition: opacity 0.3s ease;
|
||
z-index: 10;
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 4px solid rgba(107, 70, 193, 0.2);
|
||
border-top: 4px solid #6B46C1;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 14px;
|
||
color: #6B46C1;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 功能按钮区域样式 */
|
||
.action-buttons {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
padding: 12px;
|
||
background: #f8f9fa;
|
||
border-top: 1px solid #e9ecef;
|
||
}
|
||
|
||
.action-btn {
|
||
flex: 1;
|
||
min-width: calc(20% - 8px);
|
||
margin: 4px;
|
||
padding: 12px 8px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
|
||
color: #ffffff;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
text-align: center;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.action-btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.3);
|
||
}
|
||
|
||
.action-btn:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.action-btn.delete-btn {
|
||
background: linear-gradient(135deg, #EF4444 0%, #F87171 100%);
|
||
}
|
||
|
||
.action-btn.delete-btn:hover {
|
||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||
}
|
||
|
||
.btn-icon {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.btn-text {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.shu-card-container {
|
||
margin: 8px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.action-buttons {
|
||
padding: 8px;
|
||
}
|
||
|
||
.action-btn {
|
||
padding: 10px 6px;
|
||
min-width: calc(20% - 6px);
|
||
margin: 3px;
|
||
}
|
||
|
||
.btn-icon {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.btn-text {
|
||
font-size: 11px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.shu-card-container {
|
||
margin: 4px;
|
||
}
|
||
|
||
.action-btn {
|
||
padding: 8px 4px;
|
||
}
|
||
|
||
.btn-icon {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.btn-text {
|
||
font-size: 10px;
|
||
}
|
||
}
|
||
|
||
/* 动画效果 */
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
</style>
|
||
```
|
||
|
||
### 2. 集成到父组件 `CreateProjectShu.vue`
|
||
- 在父组件中引入 `shu.vue` 组件
|
||
- 实现事件处理逻辑,接收子组件抛出的事件
|
||
- 在模板中使用新组件展示卡片
|
||
|
||
## 设计规范
|
||
- 主色调:深紫色(#6B46C1)、浅紫色(#A78BFA)
|
||
- 辅助色:深灰色(#1F2937)、浅灰色(#F3F4F6)
|
||
- 按钮样式:圆角设计(8px半径)、微妙阴影、悬停效果
|
||
- 字体排版:Inter字体系列、16px基础大小、响应式缩放
|
||
- 布局风格:基于卡片的设计、统一间距(8px网格系统)
|
||
- 动画效果:平滑过渡(200ms缓入缓出)、加载蒙层
|
||
|
||
## 测试与验证
|
||
- 确保组件在竖屏移动端正常显示
|
||
- 测试图片加载、预览功能
|
||
- 验证按钮事件正确抛出
|
||
- 检查响应式布局适配情况 |