hookehuyr

feat(用户资料): 优化头像上传和预览功能

- 添加头像上传至服务器的功能
- 重构头像预览组件为通用图片预览
- 使用 NutConfigProvider 统一性别选择样式
- 将 @click 事件改为 @tap 以兼容移动端
...@@ -11,6 +11,7 @@ declare module 'vue' { ...@@ -11,6 +11,7 @@ declare module 'vue' {
11 NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet'] 11 NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet']
12 NutButton: typeof import('@nutui/nutui-taro')['Button'] 12 NutButton: typeof import('@nutui/nutui-taro')['Button']
13 NutCol: typeof import('@nutui/nutui-taro')['Col'] 13 NutCol: typeof import('@nutui/nutui-taro')['Col']
14 + NutConfigProvider: typeof import('@nutui/nutui-taro')['ConfigProvider']
14 NutDatePicker: typeof import('@nutui/nutui-taro')['DatePicker'] 15 NutDatePicker: typeof import('@nutui/nutui-taro')['DatePicker']
15 NutDialog: typeof import('@nutui/nutui-taro')['Dialog'] 16 NutDialog: typeof import('@nutui/nutui-taro')['Dialog']
16 NutForm: typeof import('@nutui/nutui-taro')['Form'] 17 NutForm: typeof import('@nutui/nutui-taro')['Form']
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
7 :src="formData.avatar || defaultAvatar" 7 :src="formData.avatar || defaultAvatar"
8 class="avatar-image" 8 class="avatar-image"
9 mode="aspectFill" 9 mode="aspectFill"
10 - @click="previewAvatar" 10 + @tap="previewAvatar(formData.avatar || defaultAvatar)"
11 /> 11 />
12 </view> 12 </view>
13 <view class="change-avatar-btn" @click="changeAvatar"> 13 <view class="change-avatar-btn" @click="changeAvatar">
...@@ -36,12 +36,14 @@ ...@@ -36,12 +36,14 @@
36 </nut-form-item> 36 </nut-form-item>
37 37
38 <!-- 性别 --> 38 <!-- 性别 -->
39 - <nut-form-item label="性别" prop="gender" body-align="right"> 39 + <nut-config-provider :theme-vars="themeVars">
40 - <nut-radio-group v-model="formData.gender" direction="horizontal"> 40 + <nut-form-item label="性别" prop="gender" body-align="right">
41 - <nut-radio label="男">男</nut-radio> 41 + <nut-radio-group v-model="formData.gender" direction="horizontal">
42 - <nut-radio label="女">女</nut-radio> 42 + <nut-radio label="男">男</nut-radio>
43 - </nut-radio-group> 43 + <nut-radio label="女">女</nut-radio>
44 - </nut-form-item> 44 + </nut-radio-group>
45 + </nut-form-item>
46 + </nut-config-provider>
45 47
46 <!-- 生日 --> 48 <!-- 生日 -->
47 <nut-form-item label="生日" prop="birthday"> 49 <nut-form-item label="生日" prop="birthday">
...@@ -156,12 +158,8 @@ ...@@ -156,12 +158,8 @@
156 /> 158 />
157 </nut-popup> 159 </nut-popup>
158 160
159 - <!-- 头像预览 --> 161 + <!-- 图片预览组件 -->
160 - <nut-image-preview 162 + <nut-image-preview v-model:show="previewVisible" :images="previewImages" :init-no="previewIndex" @close="closePreview" />
161 - v-model:show="avatarPreviewVisible"
162 - :images="[formData.avatar || defaultAvatar]"
163 - @close="closePreview"
164 - />
165 163
166 <!-- 成功提示 --> 164 <!-- 成功提示 -->
167 <nut-toast 165 <nut-toast
...@@ -177,6 +175,12 @@ import { ref, reactive, onMounted } from 'vue' ...@@ -177,6 +175,12 @@ import { ref, reactive, onMounted } from 'vue'
177 import Taro from '@tarojs/taro' 175 import Taro from '@tarojs/taro'
178 import './index.less' 176 import './index.less'
179 import { Right } from '@nutui/icons-vue-taro' 177 import { Right } from '@nutui/icons-vue-taro'
178 +import BASE_URL from '@/utils/config';
179 +
180 +// 主题配置
181 +const themeVars = {
182 + radioLabelFontActiveColor: '#f97316'
183 +}
180 184
181 // 默认头像 185 // 默认头像
182 const defaultAvatar = 'https://randomuser.me/api/portraits/men/32.jpg' 186 const defaultAvatar = 'https://randomuser.me/api/portraits/men/32.jpg'
...@@ -196,11 +200,15 @@ const formData = reactive({ ...@@ -196,11 +200,15 @@ const formData = reactive({
196 const phoneDialogVisible = ref(false) 200 const phoneDialogVisible = ref(false)
197 const datePickerVisible = ref(false) 201 const datePickerVisible = ref(false)
198 const schoolPickerVisible = ref(false) 202 const schoolPickerVisible = ref(false)
199 -const avatarPreviewVisible = ref(false)
200 const toastVisible = ref(false) 203 const toastVisible = ref(false)
201 204
205 +// 图片预览相关
206 +const previewVisible = ref(false)
207 +const previewImages = ref([])
208 +const previewIndex = ref(0)
209 +
202 const closePreview = () => { 210 const closePreview = () => {
203 - avatarPreviewVisible.value = false 211 + previewVisible.value = false
204 } 212 }
205 213
206 // 手机号相关 214 // 手机号相关
...@@ -234,18 +242,79 @@ const goBack = () => { ...@@ -234,18 +242,79 @@ const goBack = () => {
234 // 更换头像 242 // 更换头像
235 const changeAvatar = () => { 243 const changeAvatar = () => {
236 Taro.chooseImage({ 244 Taro.chooseImage({
237 - count: 1, 245 + count: 1,
238 - sizeType: ['compressed'], 246 + sizeType: ['compressed'],
239 - sourceType: ['album', 'camera'], 247 + sourceType: ['album', 'camera'],
240 - success: (res) => { 248 + success: function (res) {
241 - formData.avatar = res.tempFilePaths[0] 249 + const tempFilePath = res.tempFilePaths[0]
242 - } 250 + uploadImage(tempFilePath)
243 - }) 251 + },
252 + fail: function () {
253 + Taro.showToast({
254 + title: '选择图片失败',
255 + icon: 'none'
256 + })
257 + }
258 + })
244 } 259 }
245 260
246 -// 预览头像 261 +/**
247 -const previewAvatar = () => { 262 + * 上传图片到服务器
248 - avatarPreviewVisible.value = true 263 + * @param {String} filePath - 图片文件路径
264 + */
265 +const uploadImage = (filePath) => {
266 + // 显示上传中提示
267 + Taro.showLoading({
268 + title: '上传中',
269 + mask: true
270 + })
271 +
272 + // 获取上传URL
273 + wx.uploadFile({
274 + url: BASE_URL + '/admin/?m=srv&a=upload',
275 + filePath,
276 + name: 'file',
277 + header: {
278 + 'content-type': 'multipart/form-data',
279 + },
280 + success: function (res) {
281 + let upload_data = JSON.parse(res.data);
282 + Taro.hideLoading({
283 + success: () => {
284 + if (res.statusCode === 200) {
285 + formData.avatar = upload_data.data.src;
286 + Taro.showToast({
287 + title: '上传成功',
288 + icon: 'success'
289 + })
290 + } else {
291 + Taro.showToast({
292 + icon: 'error',
293 + title: '服务器错误,稍后重试!',
294 + mask: true
295 + })
296 + }
297 + },
298 + });
299 + }
300 + });
301 +}
302 +
303 +/**
304 + * 预览头像
305 + * @param {string} imageUrl - 图片URL
306 + */
307 +const previewAvatar = (imageUrl) => {
308 + if (!imageUrl) {
309 + Taro.showToast({
310 + title: '暂无图片可预览',
311 + icon: 'none'
312 + })
313 + return
314 + }
315 + previewImages.value = [{ src: imageUrl }]
316 + previewIndex.value = 0
317 + previewVisible.value = true
249 } 318 }
250 319
251 // 显示手机号弹框 320 // 显示手机号弹框
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
7 :src="formData.avatar || defaultAvatar" 7 :src="formData.avatar || defaultAvatar"
8 class="avatar-image" 8 class="avatar-image"
9 mode="aspectFill" 9 mode="aspectFill"
10 - @click="previewAvatar" 10 + @tap="previewAvatar(formData.avatar || defaultAvatar)"
11 /> 11 />
12 </view> 12 </view>
13 <view class="change-avatar-btn" @click="changeAvatar"> 13 <view class="change-avatar-btn" @click="changeAvatar">
...@@ -64,12 +64,14 @@ ...@@ -64,12 +64,14 @@
64 </nut-form-item> 64 </nut-form-item>
65 65
66 <!-- 性别 --> 66 <!-- 性别 -->
67 - <nut-form-item label="性别" prop="gender" body-align="right" required :rules="[{ required: true, message: '请选择性别' }]"> 67 + <nut-config-provider :theme-vars="themeVars">
68 - <nut-radio-group v-model="formData.gender" direction="horizontal"> 68 + <nut-form-item label="性别" prop="gender" body-align="right" required :rules="[{ required: true, message: '请选择性别' }]">
69 - <nut-radio label="男">男</nut-radio> 69 + <nut-radio-group v-model="formData.gender" direction="horizontal">
70 - <nut-radio label="女">女</nut-radio> 70 + <nut-radio label="男">男</nut-radio>
71 - </nut-radio-group> 71 + <nut-radio label="女">女</nut-radio>
72 - </nut-form-item> 72 + </nut-radio-group>
73 + </nut-form-item>
74 + </nut-config-provider>
73 75
74 <!-- 生日 --> 76 <!-- 生日 -->
75 <nut-form-item label="生日" prop="birthday" label-position="top"> 77 <nut-form-item label="生日" prop="birthday" label-position="top">
...@@ -124,11 +126,7 @@ ...@@ -124,11 +126,7 @@
124 </nut-popup> 126 </nut-popup>
125 127
126 <!-- 头像预览 --> 128 <!-- 头像预览 -->
127 - <nut-image-preview 129 + <nut-image-preview v-model:show="previewVisible" :images="previewImages" :init-no="previewIndex" @close="closePreview" />
128 - v-model:show="avatarPreviewVisible"
129 - :images="[formData.avatar || defaultAvatar]"
130 - @close="closePreview"
131 - />
132 130
133 <!-- 成功提示 --> 131 <!-- 成功提示 -->
134 <nut-toast 132 <nut-toast
...@@ -142,13 +140,13 @@ ...@@ -142,13 +140,13 @@
142 <script setup> 140 <script setup>
143 import { ref, reactive, computed, onMounted } from 'vue' 141 import { ref, reactive, computed, onMounted } from 'vue'
144 import Taro from '@tarojs/taro' 142 import Taro from '@tarojs/taro'
145 -import { RectLeft, Right } from '@nutui/icons-vue-taro' 143 +import { Right } from '@nutui/icons-vue-taro'
146 import './index.less' 144 import './index.less'
145 +import BASE_URL from '@/utils/config';
147 146
148 // 主题配置 147 // 主题配置
149 const themeVars = { 148 const themeVars = {
150 - navbarBackground: '#f97316', 149 + radioLabelFontActiveColor: '#f97316'
151 - navbarColor: '#fff'
152 } 150 }
153 151
154 // 默认头像 152 // 默认头像
...@@ -168,13 +166,17 @@ const formData = reactive({ ...@@ -168,13 +166,17 @@ const formData = reactive({
168 // 弹框控制 166 // 弹框控制
169 const datePickerVisible = ref(false) 167 const datePickerVisible = ref(false)
170 const schoolPickerVisible = ref(false) 168 const schoolPickerVisible = ref(false)
171 -const avatarPreviewVisible = ref(false)
172 const toastVisible = ref(false) 169 const toastVisible = ref(false)
173 const toastMessage = ref('') 170 const toastMessage = ref('')
174 const toastType = ref('success') 171 const toastType = ref('success')
175 172
173 +// 图片预览相关
174 +const previewVisible = ref(false)
175 +const previewImages = ref([])
176 +const previewIndex = ref(0)
177 +
176 const closePreview = () => { 178 const closePreview = () => {
177 - avatarPreviewVisible.value = false 179 + previewVisible.value = false
178 } 180 }
179 181
180 // 验证码相关 182 // 验证码相关
...@@ -214,31 +216,82 @@ const isPhoneValid = computed(() => { ...@@ -214,31 +216,82 @@ const isPhoneValid = computed(() => {
214 }) 216 })
215 217
216 /** 218 /**
217 - * 返回上一页 219 + * 更换头像
218 */ 220 */
219 -const goBack = () => { 221 +const changeAvatar = () => {
220 - Taro.navigateBack() 222 + Taro.chooseImage({
223 + count: 1,
224 + sizeType: ['compressed'],
225 + sourceType: ['album', 'camera'],
226 + success: function (res) {
227 + const tempFilePath = res.tempFilePaths[0]
228 + uploadImage(tempFilePath)
229 + },
230 + fail: function () {
231 + Taro.showToast({
232 + title: '选择图片失败',
233 + icon: 'none'
234 + })
235 + }
236 + })
221 } 237 }
222 238
223 /** 239 /**
224 - * 更换头像 240 + * 上传图片到服务器
241 + * @param {String} filePath - 图片文件路径
225 */ 242 */
226 -const changeAvatar = () => { 243 +const uploadImage = (filePath) => {
227 - Taro.chooseImage({ 244 + // 显示上传中提示
228 - count: 1, 245 + Taro.showLoading({
229 - sizeType: ['compressed'], 246 + title: '上传中',
230 - sourceType: ['album', 'camera'], 247 + mask: true
231 - success: (res) => { 248 + })
232 - formData.avatar = res.tempFilePaths[0] 249 +
233 - } 250 + // 获取上传URL
234 - }) 251 + wx.uploadFile({
252 + url: BASE_URL + '/admin/?m=srv&a=upload',
253 + filePath,
254 + name: 'file',
255 + header: {
256 + 'content-type': 'multipart/form-data',
257 + },
258 + success: function (res) {
259 + let upload_data = JSON.parse(res.data);
260 + Taro.hideLoading({
261 + success: () => {
262 + if (res.statusCode === 200) {
263 + formData.avatar = upload_data.data.src;
264 + Taro.showToast({
265 + title: '上传成功',
266 + icon: 'success'
267 + })
268 + } else {
269 + Taro.showToast({
270 + icon: 'error',
271 + title: '服务器错误,稍后重试!',
272 + mask: true
273 + })
274 + }
275 + },
276 + });
277 + }
278 + });
235 } 279 }
236 280
237 /** 281 /**
238 * 预览头像 282 * 预览头像
239 */ 283 */
240 -const previewAvatar = () => { 284 +const previewAvatar = (imageUrl) => {
241 - avatarPreviewVisible.value = true 285 + if (!imageUrl) {
286 + Taro.showToast({
287 + title: '暂无图片可预览',
288 + icon: 'none'
289 + })
290 + return
291 + }
292 + previewImages.value = [{ src: imageUrl }]
293 + previewIndex.value = 0
294 + previewVisible.value = true
242 } 295 }
243 296
244 /** 297 /**
......