hookehuyr

feat(Feedback): 支持多图片上传并添加大小限制

refactor(JoinFamily): 使用原子类替换自定义样式
...@@ -17,31 +17,45 @@ ...@@ -17,31 +17,45 @@
17 <!-- Upload Screenshot Section --> 17 <!-- Upload Screenshot Section -->
18 <view class="mb-6"> 18 <view class="mb-6">
19 <view class="text-lg font-medium mb-2">上传截图 (可选)</view> 19 <view class="text-lg font-medium mb-2">上传截图 (可选)</view>
20 - <view v-if="screenshot" class="mb-4"> 20 +
21 - <view class="relative inline-block"> 21 + <!-- 已上传图片显示 -->
22 + <view class="flex flex-wrap gap-2 mb-4">
23 + <view
24 + v-for="(item, index) in screenshots"
25 + :key="index"
26 + class="relative"
27 + >
22 <image 28 <image
23 - :src="screenshot" 29 + :src="item.url"
24 class="w-24 h-24 rounded-lg object-cover" 30 class="w-24 h-24 rounded-lg object-cover"
25 mode="aspectFill" 31 mode="aspectFill"
26 - @tap="previewImage" 32 + @tap="() => previewImage(index)"
27 /> 33 />
28 <view 34 <view
29 - @click="deleteImage" 35 + @click="() => deleteImage(index)"
30 - class="absolute -top-2 -right-2 w-5 h-5 bg-red-500 rounded-full flex items-center justify-center" 36 + class="absolute -top-2 -right-2 w-4 h-4 bg-red-500 rounded-full flex items-center justify-center"
31 > 37 >
32 <view class="text-white text-xs">×</view> 38 <view class="text-white text-xs">×</view>
33 </view> 39 </view>
34 </view> 40 </view>
35 - </view> 41 + <!-- 上传按钮 -->
36 <view 42 <view
37 - v-if="!screenshot" 43 + v-if="screenshots.length < maxImages"
38 - class="border border-dashed border-gray-300 rounded-lg p-6 flex flex-col items-center justify-center" 44 + class="w-24 h-24 border border-dashed border-gray-300 rounded-lg flex flex-col items-center justify-center"
39 @click="chooseImage" 45 @click="chooseImage"
40 > 46 >
41 <view class="text-gray-400 mb-2"> 47 <view class="text-gray-400 mb-2">
42 <Photograph size="24" /> 48 <Photograph size="24" />
43 </view> 49 </view>
44 - <view class="text-center text-gray-400">添加图片</view> 50 + <view class="text-center text-gray-400 text-xs">添加图片</view>
51 + </view>
52 + </view>
53 +
54 + <!-- 提示信息 -->
55 + <view class="text-xs text-gray-500 mt-2">
56 + <view>• 图片大小不能超过10MB</view>
57 + <view>• 最多只能上传3张图片</view>
58 + <view>• 当前已上传 {{ screenshots.length }}/{{ maxImages }} 张</view>
45 </view> 59 </view>
46 </view> 60 </view>
47 61
...@@ -54,6 +68,7 @@ ...@@ -54,6 +68,7 @@
54 v-model="name" 68 v-model="name"
55 class="w-full text-gray-600 focus:outline-none" 69 class="w-full text-gray-600 focus:outline-none"
56 placeholder="请输入您的姓名" 70 placeholder="请输入您的姓名"
71 + :cursorSpacing="100"
57 /> 72 />
58 </view> 73 </view>
59 </view> 74 </view>
...@@ -67,6 +82,7 @@ ...@@ -67,6 +82,7 @@
67 v-model="contact" 82 v-model="contact"
68 class="w-full text-gray-600 focus:outline-none" 83 class="w-full text-gray-600 focus:outline-none"
69 placeholder="请输入您的手机号或微信号" 84 placeholder="请输入您的手机号或微信号"
85 + :cursorSpacing="100"
70 /> 86 />
71 </view> 87 </view>
72 </view> 88 </view>
...@@ -94,13 +110,15 @@ ...@@ -94,13 +110,15 @@
94 import { ref } from 'vue'; 110 import { ref } from 'vue';
95 import Taro from '@tarojs/taro'; 111 import Taro from '@tarojs/taro';
96 import { Photograph } from '@nutui/icons-vue-taro'; 112 import { Photograph } from '@nutui/icons-vue-taro';
113 +import BASE_URL from '@/utils/config';
97 114
98 const feedbackText = ref(''); 115 const feedbackText = ref('');
99 -const screenshot = ref(''); 116 +const screenshots = ref([]);
100 const name = ref(''); 117 const name = ref('');
101 const contact = ref(''); 118 const contact = ref('');
102 const previewVisible = ref(false); 119 const previewVisible = ref(false);
103 const previewImages = ref([]); 120 const previewImages = ref([]);
121 +const maxImages = 3;
104 122
105 /** 123 /**
106 * 显示提示信息 124 * 显示提示信息
...@@ -118,13 +136,34 @@ const showToast = (message, type = 'success') => { ...@@ -118,13 +136,34 @@ const showToast = (message, type = 'success') => {
118 * 选择图片 136 * 选择图片
119 */ 137 */
120 const chooseImage = () => { 138 const chooseImage = () => {
139 + if (screenshots.value.length >= maxImages) {
140 + showToast(`最多只能上传${maxImages}张图片`, 'error');
141 + return;
142 + }
143 +
144 + const remainingCount = maxImages - screenshots.value.length;
121 Taro.chooseImage({ 145 Taro.chooseImage({
122 - count: 1, 146 + count: remainingCount,
123 sizeType: ['compressed'], 147 sizeType: ['compressed'],
124 sourceType: ['album', 'camera'], 148 sourceType: ['album', 'camera'],
125 success: function (res) { 149 success: function (res) {
126 - const tempFilePath = res.tempFilePaths[0]; 150 + res.tempFilePaths.forEach(tempFilePath => {
127 - screenshot.value = tempFilePath; 151 + // 检查文件大小(10MB = 10 * 1024 * 1024 bytes)
152 + Taro.getFileInfo({
153 + filePath: tempFilePath,
154 + success: function (fileInfo) {
155 + if (fileInfo.size > 10 * 1024 * 1024) {
156 + showToast('图片大小不能超过10MB', 'error');
157 + return;
158 + }
159 + uploadImage(tempFilePath);
160 + },
161 + fail: function () {
162 + // 如果获取文件信息失败,直接上传
163 + uploadImage(tempFilePath);
164 + }
165 + });
166 + });
128 }, 167 },
129 fail: function () { 168 fail: function () {
130 showToast('选择图片失败', 'error'); 169 showToast('选择图片失败', 'error');
...@@ -133,10 +172,53 @@ const chooseImage = () => { ...@@ -133,10 +172,53 @@ const chooseImage = () => {
133 }; 172 };
134 173
135 /** 174 /**
175 + * 上传图片到服务器
176 + */
177 +const uploadImage = (filePath) => {
178 + // 显示上传中提示
179 + Taro.showLoading({
180 + title: '上传中',
181 + mask: true
182 + });
183 +
184 + wx.uploadFile({
185 + url: BASE_URL + '/admin/?m=srv&a=upload',
186 + filePath,
187 + name: 'file',
188 + header: {
189 + 'content-type': 'multipart/form-data',
190 + },
191 + success: function (res) {
192 + let upload_data = JSON.parse(res.data);
193 + Taro.hideLoading({
194 + success: () => {
195 + if (res.statusCode === 200) {
196 + screenshots.value.push({
197 + url: upload_data.data.src,
198 + localPath: filePath
199 + });
200 + showToast('上传成功', 'success');
201 + } else {
202 + showToast('服务器错误,稍后重试!', 'error');
203 + }
204 + },
205 + });
206 + },
207 + fail: function (res) {
208 + Taro.hideLoading({
209 + success: () => {
210 + showToast('上传失败,稍后重试!', 'error');
211 + }
212 + });
213 + }
214 + });
215 +};
216 +
217 +/**
136 * 预览图片 218 * 预览图片
137 */ 219 */
138 -const previewImage = () => { 220 +const previewImage = (index) => {
139 - previewImages.value = [{ src: screenshot.value }]; 221 + previewImages.value = screenshots.value.map(item => ({ src: item.url }));
140 previewVisible.value = true; 222 previewVisible.value = true;
141 }; 223 };
142 224
...@@ -150,8 +232,9 @@ const closePreview = () => { ...@@ -150,8 +232,9 @@ const closePreview = () => {
150 /** 232 /**
151 * 删除图片 233 * 删除图片
152 */ 234 */
153 -const deleteImage = () => { 235 +const deleteImage = (index) => {
154 - screenshot.value = ''; 236 + screenshots.value.splice(index, 1);
237 + showToast('图片已删除', 'success');
155 }; 238 };
156 239
157 /** 240 /**
...@@ -176,7 +259,7 @@ const submitFeedback = () => { ...@@ -176,7 +259,7 @@ const submitFeedback = () => {
176 259
177 // 提交成功后清空表单 260 // 提交成功后清空表单
178 feedbackText.value = ''; 261 feedbackText.value = '';
179 - screenshot.value = ''; 262 + screenshots.value = [];
180 name.value = ''; 263 name.value = '';
181 contact.value = ''; 264 contact.value = '';
182 }; 265 };
......
...@@ -36,34 +36,3 @@ ...@@ -36,34 +36,3 @@
36 font-weight: 700; 36 font-weight: 700;
37 margin-bottom: 1rem; 37 margin-bottom: 1rem;
38 } 38 }
...\ No newline at end of file ...\ No newline at end of file
39 -
40 -.identity-grid {
41 - display: grid;
42 - grid-template-columns: repeat(2, 1fr);
43 - gap: 1rem;
44 -}
45 -
46 -.identity-item {
47 - display: flex;
48 - align-items: center;
49 - justify-content: center;
50 - padding: 1rem;
51 - border: 1px solid #e5e7eb;
52 - border-radius: 0.5rem;
53 -}
54 -
55 -.identity-item-selected {
56 - border-color: #3b82f6;
57 - background-color: #eff6ff;
58 -}
59 -
60 -.identity-item-content {
61 - display: flex;
62 - flex-direction: column;
63 - align-items: center;
64 -}
65 -
66 -.identity-item-icon {
67 - font-size: 1.5rem;
68 - margin-bottom: 0.5rem;
69 -}
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
33 </view> 33 </view>
34 </view> 34 </view>
35 <!-- Help text --> 35 <!-- Help text -->
36 - <view class="text-gray-500 text-center text-sm mb-8"> 36 + <view class="text-gray-500 text-center text-sm mb-4">
37 没有口令?请联系您的大家长获取 37 没有口令?请联系您的大家长获取
38 </view> 38 </view>
39 <!-- Role selection --> 39 <!-- Role selection -->
...@@ -41,20 +41,20 @@ ...@@ -41,20 +41,20 @@
41 <h3 class="identity-title"> 41 <h3 class="identity-title">
42 选择您的身份 42 选择您的身份
43 </h3> 43 </h3>
44 - <view class="identity-grid"> 44 + <view class="flex gap-2 flex-wrap">
45 <view 45 <view
46 v-for="role in familyRoles" 46 v-for="role in familyRoles"
47 :key="role.id" 47 :key="role.id"
48 + @click="selectedRole = role.id"
48 :class="[ 49 :class="[
49 - 'identity-item', 50 + 'w-[calc(49%-4rpx)] py-3 rounded-lg border text-center flex flex-col items-center gap-1',
50 - { 'identity-item-selected': selectedRole === role.id } 51 + selectedRole === role.id
52 + ? 'border-blue-500 bg-blue-50 text-blue-500'
53 + : 'border-gray-200 text-gray-700'
51 ]" 54 ]"
52 - @click="selectedRole = role.id"
53 > 55 >
54 - <view class="identity-item-content"> 56 + <My size="20" />
55 - <My size="20" class="identity-item-icon" /> 57 + <span class="text-sm">{{ role.label }}</span>
56 - <span class="identity-item-label">{{ role.label }}</span>
57 - </view>
58 </view> 58 </view>
59 </view> 59 </view>
60 </view> 60 </view>
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
63 @click="handleJoinFamily" 63 @click="handleJoinFamily"
64 :disabled="!isComplete" 64 :disabled="!isComplete"
65 :class="[ 65 :class="[
66 - 'w-full py-4 text-white text-lg font-medium rounded-lg mt-auto text-center', 66 + 'w-full py-3 text-white text-lg font-medium rounded-lg mt-auto text-center',
67 isComplete ? 'bg-blue-500' : 'bg-gray-300' 67 isComplete ? 'bg-blue-500' : 'bg-gray-300'
68 ]" 68 ]"
69 > 69 >
......