hookehuyr

feat(上传功能): 添加媒体上传组件和弹窗封装

实现完整的媒体上传功能,包括:
1. 新增 PopupWrapper 作为通用弹窗组件
2. 新增 UploadMediaPopup 作为上传弹窗容器
3. 新增 UploadMediaComponent 实现具体上传逻辑
4. 在 FamilyAlbum 中集成上传功能
5. 更新组件类型声明

支持图片和视频上传,包含预览、删除和提交功能
...@@ -29,6 +29,7 @@ declare module 'vue' { ...@@ -29,6 +29,7 @@ declare module 'vue' {
29 NutToast: typeof import('@nutui/nutui-taro')['Toast'] 29 NutToast: typeof import('@nutui/nutui-taro')['Toast']
30 Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] 30 Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
31 PointsCollector: typeof import('./src/components/PointsCollector.vue')['default'] 31 PointsCollector: typeof import('./src/components/PointsCollector.vue')['default']
32 + PopupWrapper: typeof import('./src/components/PopupWrapper.vue')['default']
32 PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] 33 PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
33 PrimaryButton: typeof import('./src/components/PrimaryButton.vue')['default'] 34 PrimaryButton: typeof import('./src/components/PrimaryButton.vue')['default']
34 RankingCard: typeof import('./src/components/RankingCard.vue')['default'] 35 RankingCard: typeof import('./src/components/RankingCard.vue')['default']
...@@ -37,6 +38,8 @@ declare module 'vue' { ...@@ -37,6 +38,8 @@ declare module 'vue' {
37 ShareButton: typeof import('./src/components/ShareButton/index.vue')['default'] 38 ShareButton: typeof import('./src/components/ShareButton/index.vue')['default']
38 TabBar: typeof import('./src/components/TabBar.vue')['default'] 39 TabBar: typeof import('./src/components/TabBar.vue')['default']
39 TotalPointsDisplay: typeof import('./src/components/TotalPointsDisplay.vue')['default'] 40 TotalPointsDisplay: typeof import('./src/components/TotalPointsDisplay.vue')['default']
41 + UploadMediaComponent: typeof import('./src/components/UploadMediaComponent.vue')['default']
42 + UploadMediaPopup: typeof import('./src/components/UploadMediaPopup.vue')['default']
40 WeRunAuth: typeof import('./src/components/WeRunAuth.vue')['default'] 43 WeRunAuth: typeof import('./src/components/WeRunAuth.vue')['default']
41 } 44 }
42 } 45 }
......
...@@ -109,6 +109,15 @@ ...@@ -109,6 +109,15 @@
109 @fullscreenchange="handleFullscreenChange" 109 @fullscreenchange="handleFullscreenChange"
110 /> 110 />
111 </view> 111 </view>
112 +
113 + <!-- 上传媒体弹窗 -->
114 + <UploadMediaPopup
115 + v-model:visible="uploadPopupVisible"
116 + from="family"
117 + :id="''"
118 + @success="handleUploadSuccess"
119 + @close="closeUploadPopup"
120 + />
112 </view> 121 </view>
113 </template> 122 </template>
114 123
...@@ -118,12 +127,16 @@ import Taro, { useDidShow } from '@tarojs/taro'; ...@@ -118,12 +127,16 @@ import Taro, { useDidShow } from '@tarojs/taro';
118 import { Close, Photograph, IconFont } from '@nutui/icons-vue-taro'; 127 import { Close, Photograph, IconFont } from '@nutui/icons-vue-taro';
119 128
120 import { getPhotoListAPI } from '@/api/photo'; 129 import { getPhotoListAPI } from '@/api/photo';
130 +import UploadMediaPopup from '@/components/UploadMediaPopup.vue';
121 131
122 // 视频播放相关状态 132 // 视频播放相关状态
123 const videoVisible = ref(false); 133 const videoVisible = ref(false);
124 const currentVideo = ref(null); 134 const currentVideo = ref(null);
125 const videoId = ref(0); 135 const videoId = ref(0);
126 136
137 +// 上传弹窗相关状态
138 +const uploadPopupVisible = ref(false);
139 +
127 /** 140 /**
128 * 处理媒体项点击事件 141 * 处理媒体项点击事件
129 * @param {Object} item - 媒体项 142 * @param {Object} item - 媒体项
...@@ -239,10 +252,35 @@ const openAlbumList = () => { ...@@ -239,10 +252,35 @@ const openAlbumList = () => {
239 }; 252 };
240 253
241 /** 254 /**
242 - * 跳转到上传媒体页面 255 + * 打开上传弹窗
243 */ 256 */
244 const navigateToUpload = () => { 257 const navigateToUpload = () => {
245 - Taro.navigateTo({ url: '/pages/UploadMedia/index' }); 258 + uploadPopupVisible.value = true;
259 +};
260 +
261 +/**
262 + * 关闭上传弹窗
263 + */
264 +const closeUploadPopup = () => {
265 + uploadPopupVisible.value = false;
266 +};
267 +
268 +/**
269 + * 上传成功回调
270 + * @param {Object} data - 上传成功的数据
271 + */
272 +const handleUploadSuccess = (data) => {
273 + console.log('上传成功:', data);
274 + // 刷新相册数据
275 + refreshData();
276 + // 关闭弹窗
277 + closeUploadPopup();
278 + // 显示成功提示
279 + Taro.showToast({
280 + title: '上传成功',
281 + icon: 'success',
282 + duration: 2000
283 + });
246 }; 284 };
247 285
248 // 组件挂载时获取数据 286 // 组件挂载时获取数据
......
...@@ -126,7 +126,7 @@ const sourceTypeMap = { ...@@ -126,7 +126,7 @@ const sourceTypeMap = {
126 'CHECK_IN_COUNT': '完成活动', 126 'CHECK_IN_COUNT': '完成活动',
127 'FAMILY_SIZE': '家庭成员', 127 'FAMILY_SIZE': '家庭成员',
128 'COMPANION_PHOTO': '陪伴拍照', 128 'COMPANION_PHOTO': '陪伴拍照',
129 - 'WHEELCHAIR_COMPANION': '陪伴轮椅' 129 + 'WHEELCHAIR_COMPANION': '特殊陪伴'
130 } 130 }
131 131
132 /** 132 /**
......
1 +<template>
2 +<nut-popup
3 + :visible="visibleModel"
4 + :position="position"
5 + :style="popupStyle"
6 + :closeable="closeable"
7 + :close-on-click-overlay="closeOnClickOverlay"
8 + :round="round"
9 + :z-index="zIndex"
10 + @close="handleClose"
11 + @open="handleOpen"
12 + @opened="handleOpened"
13 + @closed="handleClosed"
14 +>
15 + <!-- 自定义头部 -->
16 + <view v-if="showHeader" class="popup-header">
17 + <view class="flex items-center justify-between p-4 border-b border-gray-200">
18 + <view class="text-lg font-medium">{{ title }}</view>
19 + <view v-if="showCloseButton" @tap="handleClose" class="w-8 h-8 flex items-center justify-center"></view>
20 + </view>
21 + </view>
22 +
23 + <!-- 内容区域 -->
24 + <view class="popup-content" :style="contentStyle">
25 + <slot></slot>
26 + </view>
27 +</nut-popup>
28 +</template>
29 +
30 +<script setup>
31 +import { ref, computed, defineProps, defineEmits } from 'vue';
32 +import { Close } from '@nutui/icons-vue-taro';
33 +
34 +// 定义props
35 +const props = defineProps({
36 + // 控制弹窗显示/隐藏
37 + visible: {
38 + type: Boolean,
39 + default: false
40 + },
41 + // 弹窗位置
42 + position: {
43 + type: String,
44 + default: 'center',
45 + validator: (value) => ['center', 'top', 'bottom', 'left', 'right'].includes(value)
46 + },
47 + // 弹窗标题
48 + title: {
49 + type: String,
50 + default: ''
51 + },
52 + // 是否显示头部
53 + showHeader: {
54 + type: Boolean,
55 + default: true
56 + },
57 + // 是否显示关闭按钮
58 + showCloseButton: {
59 + type: Boolean,
60 + default: true
61 + },
62 + // 是否可关闭
63 + closeable: {
64 + type: Boolean,
65 + default: true
66 + },
67 + // 点击遮罩是否关闭
68 + closeOnClickOverlay: {
69 + type: Boolean,
70 + default: false
71 + },
72 + // 是否圆角
73 + round: {
74 + type: Boolean,
75 + default: false
76 + },
77 + // 层级
78 + zIndex: {
79 + type: Number,
80 + default: 2000
81 + },
82 + // 自定义宽度
83 + width: {
84 + type: String,
85 + default: '100%'
86 + },
87 + // 自定义高度
88 + height: {
89 + type: String,
90 + default: '100%'
91 + },
92 + // 是否全屏
93 + fullscreen: {
94 + type: Boolean,
95 + default: false
96 + }
97 +});
98 +
99 +// 定义emits
100 +const emit = defineEmits(['update:visible', 'close', 'open', 'opened', 'closed']);
101 +
102 +// 计算属性处理双向绑定
103 +const visibleModel = computed({
104 + get() {
105 + return props.visible;
106 + },
107 + set(value) {
108 + emit('update:visible', value);
109 + }
110 +});
111 +
112 +// 计算弹窗样式
113 +const popupStyle = computed(() => {
114 + const style = {};
115 +
116 + if (props.fullscreen) {
117 + style.width = '100%';
118 + style.height = '100%';
119 + } else {
120 + if (props.width) style.width = props.width;
121 + if (props.height) style.height = props.height;
122 + }
123 +
124 + return style;
125 +});
126 +
127 +// 计算内容区域样式
128 +const contentStyle = computed(() => {
129 + const style = {};
130 +
131 + if (props.showHeader) {
132 + style.height = 'calc(100% - 60px)';
133 + } else {
134 + style.height = '100%';
135 + }
136 +
137 + return style;
138 +});
139 +
140 +/**
141 + * 处理关闭事件
142 + */
143 +const handleClose = () => {
144 + emit('update:visible', false);
145 + emit('close');
146 +};
147 +
148 +/**
149 + * 处理打开事件
150 + */
151 +const handleOpen = () => {
152 + emit('open');
153 +};
154 +
155 +/**
156 + * 处理已打开事件
157 + */
158 +const handleOpened = () => {
159 + emit('opened');
160 +};
161 +
162 +/**
163 + * 处理已关闭事件
164 + */
165 +const handleClosed = () => {
166 + emit('closed');
167 +};
168 +</script>
169 +
170 +<style lang="less" scoped>
171 +.popup-header {
172 + background-color: #fff;
173 + border-bottom: 1px solid #e5e7eb;
174 +}
175 +
176 +.popup-content {
177 + background-color: #f8f9fa;
178 + overflow-y: auto;
179 +}
180 +</style>
This diff is collapsed. Click to expand it.
1 +<template>
2 + <PopupWrapper
3 + v-model:visible="popupVisible"
4 + position="right"
5 + title="上传照片/视频"
6 + :fullscreen="true"
7 + :close-on-click-overlay="false"
8 + @close="handleClose"
9 + >
10 + <UploadMediaComponent
11 + :from="from"
12 + :id="id"
13 + @success="handleSuccess"
14 + @close="handleClose"
15 + />
16 + </PopupWrapper>
17 +</template>
18 +
19 +<script setup>
20 +import { ref, defineProps, defineEmits, watch } from 'vue';
21 +import PopupWrapper from '@/components/PopupWrapper.vue';
22 +import UploadMediaComponent from '@/components/UploadMediaComponent.vue';
23 +
24 +// 定义props
25 +const props = defineProps({
26 + // 控制弹窗显示/隐藏
27 + visible: {
28 + type: Boolean,
29 + default: false
30 + },
31 + // 来源标识
32 + from: {
33 + type: String,
34 + default: ''
35 + },
36 + // ID参数
37 + id: {
38 + type: String,
39 + default: ''
40 + }
41 +});
42 +
43 +// 定义emits
44 +const emit = defineEmits(['update:visible', 'success', 'close']);
45 +
46 +// 内部弹窗状态
47 +const popupVisible = ref(false);
48 +
49 +// 监听外部visible变化
50 +watch(() => props.visible, (newVal) => {
51 + popupVisible.value = newVal;
52 +}, { immediate: true });
53 +
54 +// 监听内部弹窗状态变化
55 +watch(popupVisible, (newVal) => {
56 + emit('update:visible', newVal);
57 +});
58 +
59 +/**
60 + * 处理上传成功
61 + * @param {Object} data - 上传成功的数据
62 + */
63 +const handleSuccess = (data) => {
64 + emit('success', data);
65 + handleClose();
66 +};
67 +
68 +/**
69 + * 处理关闭弹窗
70 + */
71 +const handleClose = () => {
72 + popupVisible.value = false;
73 + emit('close');
74 +};
75 +</script>
76 +
77 +<style lang="less" scoped>
78 +// 组件样式由PopupWrapper和UploadMediaComponent提供
79 +</style>
...\ No newline at end of file ...\ No newline at end of file