| src/components/custom-navbar/index.less | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/components/custom-navbar/index.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/feedback/hooks/use-media-upload.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/feedback/index.less | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/feedback/index.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/components/custom-navbar/index.less
@@ -21,6 +21,16 @@ display: block; width: 64px; height: 64px; &.pure { width: 24px; height: 24px; box-sizing: border-box; border-left: 4px solid #141E27; border-bottom: 4px solid #141E27; transform: rotate(45deg); margin-left: 10px; } } } .btn-app-wrap { src/components/custom-navbar/index.tsx
@@ -26,6 +26,8 @@ showAppBtn: false, // 是否显示返回app按钮 appBtnText: '返回APP', // 返回app按钮文案 showTitleColor: false, // 显示页面标题设置颜色 backIcon: '', // 自定义返回图标,未传时使用默认图标 pureBackArrow: false, // 使用纯样式返回箭头,不使用图片 }; state = { @@ -82,7 +84,7 @@ render() { // 这里需要注意:当设置 opacity 时,不是整个导航栏都透明,而是要保留左边区域的操作,比如后退 const { isIOS } = this.state; const { fixedtop, seizeSeat, background, opacity, showBack, colorTheme, title, showAppBtn, appBtnText, showTitleColor } = this.props; const { fixedtop, seizeSeat, background, opacity, showBack, colorTheme, title, showAppBtn, appBtnText, showTitleColor, backIcon, pureBackArrow } = this.props; const appInaverStyle = { 'backgroundColor': hexToRgbA(background, opacity), height: this.getAppInaverHeight() + 'Px', @@ -97,18 +99,16 @@ <View className='left'> {showBack && !showAppBtn && ( <View className='back-wrap' onClick={this.handlergoback.bind(this)}> {opacity === 0 ? ( colorTheme === 'black' ? ( <Image className='ic-left' mode='heightFix' src={BackBlack2}></Image> {pureBackArrow ? ( <View className='ic-left pure' /> ) : ( <Image className='ic-left' mode='heightFix' src={BackWhite2}></Image> ) ) : ( colorTheme === 'black' ? ( <Image className='ic-left' mode='heightFix' src={BackBlack}></Image> ) : ( <Image className='ic-left' mode='heightFix' src={BackWhite}></Image> ) <Image className='ic-left' mode='heightFix' src={backIcon || (opacity === 0 ? (colorTheme === 'black' ? BackBlack2 : BackWhite2) : (colorTheme === 'black' ? BackBlack : BackWhite))} ></Image> )} </View> )} src/robotaxi/pages/feedback/hooks/use-media-upload.ts
@@ -1,5 +1,6 @@ import Taro from '@tarojs/taro'; import { useCallback, useMemo, useState } from 'react'; import { getStorage } from '../../../../utils/tools'; /** * 上传成功后的服务端文件信息。 @@ -111,10 +112,12 @@ })); try { const finalFilePath = mediaType === 'image' ? await ensureImageSizeUnderLimit(filePath) : filePath; const otpToken = await getStorage('otp').catch(() => ''); const uploadResult = await Taro.uploadFile({ url: options.uploadUrl, filePath: finalFilePath, name: 'file' name: 'file', header: otpToken ? { 'Xp-Otp-Token': String(otpToken) } : undefined }); let responseData: any = uploadResult.data; try { @@ -124,12 +127,18 @@ } catch (error) { responseData = null; } console.log('[feedback upload] raw response:', uploadResult); console.log('[feedback upload] parsed response data:', responseData); if (uploadResult.statusCode !== 200 || responseData?.code !== 200 || !responseData?.data) { throw new Error(responseData?.msg || '上传失败'); } const payload = responseData?.data; const serverFile = { id: String(payload?.id || ''), name: String(payload?.name || ''), url: String(payload?.url || '') }; console.log('[feedback upload] normalized server file:', serverFile); if (!serverFile.id || !serverFile.url) { throw new Error('invalid upload response'); } @@ -138,11 +147,16 @@ server: serverFile, status: 'success' })); } catch (error) { } catch (error: any) { console.log('[feedback upload] upload failed:', error); updateItemById(itemId, (item) => ({ ...item, status: 'failed' })); Taro.showToast({ title: error?.message || '上传失败,请重试', icon: 'none' }); } }, [ensureImageSizeUnderLimit, options.uploadUrl, updateItemById]); src/robotaxi/pages/feedback/index.less
@@ -19,7 +19,11 @@ } .feedback-content { padding: 0 32px 48px; min-height: calc(100vh - 176px); padding: 0 44px; box-sizing: border-box; display: flex; flex-direction: column; } .feedback-type { @@ -27,6 +31,7 @@ font-size: 28px; line-height: 40px; font-weight: 500; margin-top: 50px; color: #141E27; .required-mark { @@ -36,10 +41,10 @@ } &-list { margin-top: 24px; margin-top: 28px; display: grid; grid-template-columns: 1fr 1fr; gap: 20px 24px; gap: 20px 20px; } &-item { @@ -48,7 +53,7 @@ display: flex; align-items: center; justify-content: center; border-radius: 16px; border-radius: 20px; background: rgba(255, 255, 255, 0.6); box-sizing: border-box; min-width: 0; @@ -61,7 +66,7 @@ text-overflow: ellipsis; font-size: 26px; line-height: 36px; color: rgba(20, 30, 39, 0.6); color: rgba(20, 30, 39, 0.5); } &.active { @@ -95,7 +100,7 @@ height: 196px; padding: 24px; padding-bottom: 48px; border-radius: 24px; border-radius: 32px; background: rgba(255, 255, 255, 0.6); font-size: 28px; line-height: 40px; @@ -128,7 +133,7 @@ position: relative; width: 208px; height: 208px; border-radius: 16px; border-radius: 32px; overflow: hidden; background: rgba(0, 0, 0, 0.03); } @@ -190,7 +195,7 @@ &-upload { width: 208px; height: 208px; border-radius: 16px; border-radius: 32px; background: rgba(255, 255, 255, 0.6); display: flex; flex-direction: column; @@ -199,9 +204,28 @@ gap: 8px; &-icon { font-size: 40px; line-height: 40px; color: rgba(20, 30, 39, 0.4); position: relative; width: 40px; height: 40px; &-line { position: absolute; top: 50%; left: 50%; background: rgba(20, 30, 39, 0.3); border-radius: 2px; transform: translate(-50%, -50%); &.horizontal { width: 32px; height: 4px; } &.vertical { width: 4px; height: 32px; } } } &-text { @@ -213,7 +237,6 @@ } .feedback-tip { margin-top: 48px; text-align: center; font-size: 24px; line-height: 34px; @@ -222,8 +245,8 @@ .feedback-submit-btn { margin-top: 24px; height: 96px; border-radius: 48px; height: 104px; border-radius: 1000px; display: flex; align-items: center; justify-content: center; @@ -238,6 +261,75 @@ } } .feedback-footer { margin-top: auto; padding-top: 48px; padding-bottom: calc(48px + env(safe-area-inset-bottom)); } .feedback-success { min-height: calc(100vh - 176px); padding: 120px 44px calc(48px + env(safe-area-inset-bottom)); box-sizing: border-box; display: flex; flex-direction: column; justify-content: space-between; &-main { display: flex; flex-direction: column; align-items: center; } &-icon { width: 96px; height: 96px; border-radius: 50%; background: #26323D; display: flex; align-items: center; justify-content: center; &-mark { font-size: 50px; line-height: 1; color: #F1CA85; margin-top: -4px; } } &-title { margin-top: 32px; font-size: 44px; line-height: 60px; font-weight: 500; color: #141E27; } &-desc { margin-top: 20px; display: flex; flex-direction: column; align-items: center; font-size: 30px; line-height: 44px; color: rgba(20, 30, 39, 0.7); } &-btn { width: 100%; height: 104px; border-radius: 1000px; display: flex; align-items: center; justify-content: center; font-size: 32px; font-weight: 500; color: #FFFFFF; background: #141E27; } } @keyframes feedback-media-spin { from { transform: rotate(0deg); src/robotaxi/pages/feedback/index.tsx
@@ -26,12 +26,19 @@ }; /** * 从扫码场景值中解析车辆 VIN,兼容 "vin=xxx" 或 "vin=xxx&other=1" 格式。 * 从扫码场景值中解析车辆 VIN,兼容 vin=xxx、VIN=xxx、querystring 以及纯 VIN 文本。 */ const parseVinFromScene = (scene: unknown) => { const decodedScene = decodeURIComponent(String(scene || '')); const matched = decodedScene.match(/vin=([^&]+)/); return matched ? matched[1] : ''; if (!decodedScene) { return ''; } const matched = decodedScene.match(/(?:^|[?&])vin=([^&]+)/i); if (matched?.[1]) { return matched[1]; } // 部分扫码场景会直接透传 VIN。 return decodedScene.trim(); } function FeedbackPage() { @@ -43,6 +50,7 @@ const [typeOptions, setTypeOptions] = useState<any[]>([]); // 问题类型选项 const [selectedTypeIds, setSelectedTypeIds] = useState<string[]>([]); // 已选问题类型 const [feedbackContent, setFeedbackContent] = useState(''); // 问题描述文案 const [isSubmitSuccess, setIsSubmitSuccess] = useState(false); // 是否展示提交成功态 const isSubmittingRef = useRef(false); // 同步提交锁,防止重复点击 const { @@ -58,7 +66,7 @@ const hasFeedbackContent = !!feedbackContent.trim(); const isVinMissing = isScanChannel && !vin; const canSubmit = selectedTypeIds.length > 0 && hasFeedbackContent && !isVinMissing; const canSubmit = selectedTypeIds.length > 0; /** * 切换问题类型多选状态。 @@ -100,38 +108,53 @@ } isSubmittingRef.current = true; const mediaUrlList = mediaItems .filter((item) => item.status === 'success' && item.server?.url) .map((item) => item.server!.url); const uploadedMediaItems = mediaItems.filter((item) => item.status === 'success' && item.server?.id && item.server?.url); const feedbackPictures = JSON.stringify( uploadedMediaItems.map((item) => ({ id: String(item.server?.id || ''), name: String(item.server?.name || ''), url: String(item.server?.url || '') })) ); const payload: any = { feedbackTypeId: selectedTypeIds.join(','), feedbackContent: feedbackContent.trim(), feedbackPictures: mediaUrlList.join(','), feedbackChannel }; if (uploadedMediaItems.length > 0) { payload.feedbackPictures = feedbackPictures; } if (isScanChannel) { payload.vin = vin; } console.log('[feedback submit] media items:', mediaItems); console.log('[feedback submit] feedbackPictures:', feedbackPictures); console.log('[feedback submit] payload:', payload); submitComplaint(payload).then((res: any) => { const isFailure = res?.success === false || (typeof res?.code === 'number' && res.code !== 0); if (isFailure) { Taro.showToast({ title: res?.msg || '提交失败,请稍后重试', icon: 'none' }); return; } Taro.showToast({ title: '提交成功', icon: 'none' }); if (isScanChannel) { Taro.reLaunch({ url: '/robotaxi/pages/begin-route/index' }); } else { Taro.navigateBack({ fail: () => Taro.navigateTo({ url: '/robotaxi/pages/personal-center/index' }) }); } console.log('[feedback submit] success response:', res); setIsSubmitSuccess(true); }).catch((error: any) => { Taro.showToast({ title: error?.msg || '提交失败,请稍后重试', icon: 'none' }); console.log('[feedback submit] error response:', error); const errorTitle = error?.msg || (error?.code ? `提交失败(${error.code})` : '提交失败,请稍后重试'); Taro.showToast({ title: errorTitle, icon: 'none' }); }).finally(() => { isSubmittingRef.current = false; }); }; /** * 成功态确认按钮,沿用原有提交成功后的跳转逻辑。 */ const handleSuccessConfirm = () => { if (isScanChannel) { Taro.reLaunch({ url: '/robotaxi/pages/begin-route/index' }); return; } Taro.navigateBack({ fail: () => Taro.navigateTo({ url: '/robotaxi/pages/personal-center/index' }) }); }; @@ -148,11 +171,27 @@ <CustomNavbar title='意见反馈' colorTheme='#141E27' pureBackArrow opacity={0} showTitleColor seizeSeat /> {isSubmitSuccess ? ( <View className='feedback-success'> <View className='feedback-success-main'> <View className='feedback-success-icon'> <Text className='feedback-success-icon-mark'>✓</Text> </View> <View className='feedback-success-title'>反馈成功</View> <View className='feedback-success-desc'> <Text>我们将会尽快修复和完善相关功能,</Text> <Text>感谢您的关注与支持!</Text> </View> </View> <View className='feedback-success-btn' onClick={handleSuccessConfirm}>我知道了</View> </View> ) : ( <View className='feedback-content'> <View className='feedback-type'> <View className='feedback-type-title'>问题类型<Text className='required-mark'>*</Text></View> @@ -218,19 +257,25 @@ ))} {mediaItems.length < FEEDBACK_MEDIA_MAX_COUNT && ( <View className='feedback-media-upload' onClick={chooseAndUpload}> <Text className='feedback-media-upload-icon'>+</Text> <View className='feedback-media-upload-icon'> <View className='feedback-media-upload-icon-line horizontal' /> <View className='feedback-media-upload-icon-line vertical' /> </View> <Text className='feedback-media-upload-text'>上传图片/视频</Text> </View> )} </View> <View className='feedback-tip'>您的反馈将帮助我们持续性优化 ❣️</View> <View className='feedback-footer'> <View className='feedback-tip'>您的反馈将帮助我们持续性优化 🌹</View> <View className={`feedback-submit-btn ${canSubmit ? 'active' : ''}`} onClick={handleSubmit}> 提交反馈 </View> </View> </View> )} </View> ); }