feat(反馈): 完成意见反馈模块开发和调试
新增功能: - 实现意见反馈列表页面(分页、图片预览、自动刷新) - 实现意见反馈提交页面(类型选择、图片上传、审核提示) - 添加图片上传功能(最多3张,5MB限制,支持审核) - 实现从列表页跳转提交页的导航流程 技术优化: - 修复生命周期钩子导入错误(useShow → useDidShow) - 修复图片显示错误(数组格式处理) - 使用自定义 CSS spinner 替代 NutUI Loading - 添加 useDidShow 实现返回列表时自动刷新 文档更新: - 更新 API 文档(addAPI、images 数组格式) - 更新项目 CHANGELOG Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Showing
8 changed files
with
593 additions
and
54 deletions
| ... | @@ -5,6 +5,57 @@ | ... | @@ -5,6 +5,57 @@ |
| 5 | 5 | ||
| 6 | --- | 6 | --- |
| 7 | 7 | ||
| 8 | +## [2026-02-03] - 意见反馈模块完成 | ||
| 9 | + | ||
| 10 | +### 新增 | ||
| 11 | +- 实现意见反馈列表功能 | ||
| 12 | + - 支持分页加载反馈记录 | ||
| 13 | + - 显示反馈类型、状态、内容、图片 | ||
| 14 | + - 支持图片预览功能 | ||
| 15 | + - 从提交页返回时自动刷新列表 | ||
| 16 | +- 实现意见反馈提交功能 | ||
| 17 | + - 支持选择反馈类型(功能建议/问题反馈/其他) | ||
| 18 | + - 支持上传最多 3 张图片(5MB 限制) | ||
| 19 | + - 图片审核不通过时提示用户 | ||
| 20 | + - 提交成功后返回列表页 | ||
| 21 | + | ||
| 22 | +### 修复 | ||
| 23 | +- 修复 `useShow` 导入错误,改用 `useDidShow` 生命周期钩子 | ||
| 24 | +- 修复 `onMounted` 导入错误,从 Vue 导入而非 Taro | ||
| 25 | +- 修复图片显示错误,`images` 字段改为数组格式处理 | ||
| 26 | +- 移除 NutUI Loading 组件,使用自定义 CSS spinner | ||
| 27 | + | ||
| 28 | +### 优化 | ||
| 29 | +- 调整导航流程:我的 → 反馈列表 → 提交反馈 | ||
| 30 | +- 添加页面下拉刷新支持 | ||
| 31 | +- 使用 `useDidShow` 实现列表自动刷新 | ||
| 32 | + | ||
| 33 | +### 文档 | ||
| 34 | +- 更新 API 文档 `docs/api-specs/feedback/add.md` | ||
| 35 | + - 修正接口名称为 `addAPI` | ||
| 36 | + - 更新 `images` 参数为数组格式 | ||
| 37 | + - 添加完整的代码示例 | ||
| 38 | + | ||
| 39 | +--- | ||
| 40 | + | ||
| 41 | +**详细信息**: | ||
| 42 | +- **影响文件**: | ||
| 43 | + - `src/pages/feedback-list/index.vue`(新增) | ||
| 44 | + - `src/pages/feedback-list/index.config.js`(新增) | ||
| 45 | + - `src/pages/feedback/index.vue`(更新) | ||
| 46 | + - `src/api/feedback.js`(更新) | ||
| 47 | + - `src/app.config.js`(注册路由) | ||
| 48 | + - `src/pages/mine/index.vue`(导航入口) | ||
| 49 | + - `docs/api-specs/feedback/add.md`(文档更新) | ||
| 50 | +- **技术栈**: Vue 3, Taro 4, NutUI, Composition API | ||
| 51 | +- **测试状态**: ✅ 已通过 | ||
| 52 | +- **备注**: | ||
| 53 | + - 反馈类别:1=功能建议, 3=问题反馈, 7=其他问题 | ||
| 54 | + - 图片上传使用 `Taro.uploadFile`,支持图片审核 | ||
| 55 | + - 列表使用 `useDidShow` 实现返回时自动刷新 | ||
| 56 | + | ||
| 57 | +--- | ||
| 58 | + | ||
| 8 | ## 📋 变更记录模板 | 59 | ## 📋 变更记录模板 |
| 9 | 60 | ||
| 10 | 每次添加新记录时,请使用以下标准格式: | 61 | 每次添加新记录时,请使用以下标准格式: | ... | ... |
| 1 | # 提交意见反馈 | 1 | # 提交意见反馈 |
| 2 | 2 | ||
| 3 | +## 接口说明 | ||
| 4 | + | ||
| 5 | +- **接口名称**: `addAPI` | ||
| 6 | +- **接口路径**: `/srv/?a=feedback&t=add` | ||
| 7 | +- **请求方式**: POST | ||
| 8 | +- **调用示例**: `addAPI({ category: '1', note: '反馈内容', images: 'url1,url2' })` | ||
| 9 | + | ||
| 10 | +## 请求参数 | ||
| 11 | + | ||
| 12 | +### Body 参数(application/x-www-form-urlencoded) | ||
| 13 | + | ||
| 14 | +| 参数名 | 类型 | 必填 | 说明 | 示例值 | | ||
| 15 | +|--------|------|------|------|--------| | ||
| 16 | +| category | string | 是 | 反馈类别。1=功能建议, 3=问题反馈, 7=其他问题 | `"1"` | | ||
| 17 | +| note | string | 是 | 反馈内容 | `"test"` | | ||
| 18 | +| images | array | 否 | 图片 URL 数组 | `["url1", "url2"]` | | ||
| 19 | + | ||
| 20 | +## 响应数据 | ||
| 21 | + | ||
| 22 | +### 成功响应 | ||
| 23 | + | ||
| 24 | +```json | ||
| 25 | +{ | ||
| 26 | + "code": 1, | ||
| 27 | + "msg": "提交成功" | ||
| 28 | +} | ||
| 29 | +``` | ||
| 30 | + | ||
| 31 | +### 字段说明 | ||
| 32 | + | ||
| 33 | +| 字段名 | 类型 | 说明 | | ||
| 34 | +|--------|------|------| | ||
| 35 | +| code | number | 状态码,1=成功 | | ||
| 36 | +| msg | string | 消息描述 | | ||
| 37 | + | ||
| 38 | +## 代码示例 | ||
| 39 | + | ||
| 40 | +```javascript | ||
| 41 | +import { addAPI } from '@/api/feedback' | ||
| 42 | + | ||
| 43 | +// 提交意见反馈 | ||
| 44 | +const res = await addAPI({ | ||
| 45 | + category: '1', // 反馈类别:1=功能建议 | ||
| 46 | + note: '这是反馈内容', // 反馈内容 | ||
| 47 | + images: ['url1', 'url2', 'url3'] // 图片数组(可选) | ||
| 48 | +}) | ||
| 49 | + | ||
| 50 | +if (res.code === 1) { | ||
| 51 | + console.log('提交成功') | ||
| 52 | +} | ||
| 53 | +``` | ||
| 54 | + | ||
| 55 | +## 注意事项 | ||
| 56 | + | ||
| 57 | +1. `category` 参数值说明: | ||
| 58 | + - `"1"` - 功能建议 | ||
| 59 | + - `"3"` - 问题反馈 | ||
| 60 | + - `"7"` - 其他问题 | ||
| 61 | + | ||
| 62 | +2. `images` 参数是可选的,直接传递图片 URL 数组 | ||
| 63 | + | ||
| 64 | +3. 后端统一返回格式为 `{ code, msg }`,`code === 1` 表示成功 | ||
| 65 | + | ||
| 3 | ## OpenAPI Specification | 66 | ## OpenAPI Specification |
| 4 | 67 | ||
| 5 | ```yaml | 68 | ```yaml |
| ... | @@ -53,27 +116,24 @@ paths: | ... | @@ -53,27 +116,24 @@ paths: |
| 53 | example: add | 116 | example: add |
| 54 | type: string | 117 | type: string |
| 55 | category: | 118 | category: |
| 56 | - description: 反馈类别。1=功能建议, 3=界面设计, 5=车辆新鲜, 7=其他问题 | 119 | + description: 反馈类别。1=功能建议, 3=问题反馈, 7=其他问题 |
| 57 | example: '1' | 120 | example: '1' |
| 58 | type: string | 121 | type: string |
| 59 | note: | 122 | note: |
| 60 | description: 反馈内容 | 123 | description: 反馈内容 |
| 61 | - example: '3' | 124 | + example: test |
| 62 | - type: string | ||
| 63 | - contact: | ||
| 64 | - description: 用户留下的联系方式 | ||
| 65 | - example: '3' | ||
| 66 | type: string | 125 | type: string |
| 67 | images: | 126 | images: |
| 127 | + type: array | ||
| 128 | + items: | ||
| 129 | + type: string | ||
| 68 | description: 图片 | 130 | description: 图片 |
| 69 | - type: string | ||
| 70 | required: | 131 | required: |
| 71 | - f | 132 | - f |
| 72 | - a | 133 | - a |
| 73 | - t | 134 | - t |
| 74 | - category | 135 | - category |
| 75 | - note | 136 | - note |
| 76 | - - contact | ||
| 77 | - images | 137 | - images |
| 78 | examples: {} | 138 | examples: {} |
| 79 | responses: | 139 | responses: |
| ... | @@ -99,7 +159,7 @@ paths: | ... | @@ -99,7 +159,7 @@ paths: |
| 99 | x-apifox-ordering: 0 | 159 | x-apifox-ordering: 0 |
| 100 | security: [] | 160 | security: [] |
| 101 | x-apifox-folder: 意见反馈 | 161 | x-apifox-folder: 意见反馈 |
| 102 | - x-apifox-status: developing | 162 | + x-apifox-status: testing |
| 103 | x-run-in-apifox: https://app.apifox.com/web/project/7792797/apis/api-413906671-run | 163 | x-run-in-apifox: https://app.apifox.com/web/project/7792797/apis/api-413906671-run |
| 104 | components: | 164 | components: |
| 105 | schemas: {} | 165 | schemas: {} | ... | ... |
| 1 | import { fn, fetch } from '@/api/fn'; | 1 | import { fn, fetch } from '@/api/fn'; |
| 2 | 2 | ||
| 3 | const Api = { | 3 | const Api = { |
| 4 | + Add: '/srv/?a=feedback&t=add', | ||
| 4 | List: '/srv/?a=feedback&t=list', | 5 | List: '/srv/?a=feedback&t=list', |
| 5 | - SubmitFeedback: '/srv/?a=feedback&t=add', | ||
| 6 | } | 6 | } |
| 7 | 7 | ||
| 8 | /** | 8 | /** |
| 9 | + * @description 提交意见反馈 | ||
| 10 | + * @remark | ||
| 11 | + * @param {Object} params 请求参数 | ||
| 12 | + * @param {string} params.category 反馈类别。1=功能建议, 3=问题反馈, 7=其他问题 | ||
| 13 | + * @param {string} params.note 反馈内容 | ||
| 14 | + * @param {array} params.images 图片 | ||
| 15 | + * @returns {Promise<{ | ||
| 16 | + * code: number; // 状态码 | ||
| 17 | + * msg: string; // 消息 | ||
| 18 | + * data: any; | ||
| 19 | + * }>} | ||
| 20 | + */ | ||
| 21 | +export const addAPI = (params) => fn(fetch.post(Api.Add, params)); | ||
| 22 | + | ||
| 23 | +/** | ||
| 9 | * @description 意见反馈列表 | 24 | * @description 意见反馈列表 |
| 10 | * @remark | 25 | * @remark |
| 11 | * @param {Object} params 请求参数 | 26 | * @param {Object} params 请求参数 |
| ... | @@ -29,19 +44,3 @@ const Api = { | ... | @@ -29,19 +44,3 @@ const Api = { |
| 29 | * }>} | 44 | * }>} |
| 30 | */ | 45 | */ |
| 31 | export const listAPI = (params) => fn(fetch.get(Api.List, params)); | 46 | export const listAPI = (params) => fn(fetch.get(Api.List, params)); |
| 32 | - | ||
| 33 | -/** | ||
| 34 | - * @description 提交意见反馈 | ||
| 35 | - * @remark | ||
| 36 | - * @param {Object} params 请求参数 | ||
| 37 | - * @param {string} params.category 反馈类别。1=功能建议, 3=界面设计, 5=车辆新鲜, 7=其他问题 | ||
| 38 | - * @param {string} params.note 反馈内容 | ||
| 39 | - * @param {string} params.contact 用户留下的联系方式 | ||
| 40 | - * @param {string} params.images 图片 | ||
| 41 | - * @returns {Promise<{ | ||
| 42 | - * code: number; // 状态码 | ||
| 43 | - * msg: string; // 消息 | ||
| 44 | - * data: any; | ||
| 45 | - * }>} | ||
| 46 | - */ | ||
| 47 | -export const submitFeedbackAPI = (params) => fn(fetch.post(Api.SubmitFeedback, params)); | ... | ... |
| ... | @@ -22,6 +22,7 @@ const pages = [ | ... | @@ -22,6 +22,7 @@ const pages = [ |
| 22 | 'pages/plan-submit-result/index', | 22 | 'pages/plan-submit-result/index', |
| 23 | 'pages/favorites/index', | 23 | 'pages/favorites/index', |
| 24 | 'pages/avatar/index', | 24 | 'pages/avatar/index', |
| 25 | + 'pages/feedback-list/index', | ||
| 25 | 'pages/feedback/index', | 26 | 'pages/feedback/index', |
| 26 | 'pages/login/index', | 27 | 'pages/login/index', |
| 27 | 'pages/help-center/index', | 28 | 'pages/help-center/index', | ... | ... |
src/pages/feedback-list/index.config.js
0 → 100644
src/pages/feedback-list/index.vue
0 → 100644
| 1 | +<!-- | ||
| 2 | + * @Date: 2026-02-03 | ||
| 3 | + * @Description: 意见反馈列表页面 | ||
| 4 | +--> | ||
| 5 | +<template> | ||
| 6 | + <view class="min-h-screen bg-gray-50"> | ||
| 7 | + <NavHeader title="我的反馈" /> | ||
| 8 | + | ||
| 9 | + <scroll-view | ||
| 10 | + scroll-y | ||
| 11 | + class="feedback-scroll" | ||
| 12 | + :style="{ height: scrollHeight + 'px' }" | ||
| 13 | + @scrolltolower="onScrollToLower" | ||
| 14 | + > | ||
| 15 | + <view class="p-[32rpx] pb-[250rpx]"> | ||
| 16 | + <!-- Feedback List --> | ||
| 17 | + <view v-if="loading" class="flex justify-center items-center py-[100rpx]"> | ||
| 18 | + <view class="loading-spinner"></view> | ||
| 19 | + </view> | ||
| 20 | + | ||
| 21 | + <view v-else-if="feedbackList.length === 0" class="flex flex-col items-center py-[100rpx]"> | ||
| 22 | + <text class="text-gray-400 text-[28rpx]">暂无反馈记录</text> | ||
| 23 | + </view> | ||
| 24 | + | ||
| 25 | + <view v-else class="space-y-[24rpx]"> | ||
| 26 | + <view | ||
| 27 | + v-for="item in feedbackList" | ||
| 28 | + :key="item.id" | ||
| 29 | + class="bg-white rounded-[24rpx] p-[32rpx] shadow-sm" | ||
| 30 | + > | ||
| 31 | + <!-- Header: Type & Status --> | ||
| 32 | + <view class="flex justify-between items-center mb-[20rpx]"> | ||
| 33 | + <view | ||
| 34 | + class="px-[20rpx] py-[8rpx] rounded-full text-[24rpx]" | ||
| 35 | + :class="getTypeClass(item.category)" | ||
| 36 | + > | ||
| 37 | + {{ getTypeLabel(item.category) }} | ||
| 38 | + </view> | ||
| 39 | + <view | ||
| 40 | + class="px-[20rpx] py-[8rpx] rounded-full text-[24rpx]" | ||
| 41 | + :class="item.status === 5 ? 'bg-green-100 text-green-600' : 'bg-orange-100 text-orange-600'" | ||
| 42 | + > | ||
| 43 | + {{ item.status === 5 ? '已处理' : '待处理' }} | ||
| 44 | + </view> | ||
| 45 | + </view> | ||
| 46 | + | ||
| 47 | + <!-- Content --> | ||
| 48 | + <view class="text-[28rpx] text-gray-900 mb-[20rpx] leading-relaxed"> | ||
| 49 | + {{ item.note }} | ||
| 50 | + </view> | ||
| 51 | + | ||
| 52 | + <!-- Images --> | ||
| 53 | + <view v-if="item.images && item.images.length > 0" class="flex gap-[16rpx] mb-[20rpx]"> | ||
| 54 | + <image | ||
| 55 | + v-for="(img, index) in item.images" | ||
| 56 | + :key="index" | ||
| 57 | + :src="img" | ||
| 58 | + mode="aspectFill" | ||
| 59 | + class="w-[120rpx] h-[120rpx] rounded-[12rpx]" | ||
| 60 | + @tap="previewImage(item.images, index)" | ||
| 61 | + /> | ||
| 62 | + </view> | ||
| 63 | + | ||
| 64 | + <!-- Contact --> | ||
| 65 | + <view v-if="item.contact" class="text-[24rpx] text-gray-500 mb-[20rpx]"> | ||
| 66 | + 联系方式:{{ item.contact }} | ||
| 67 | + </view> | ||
| 68 | + | ||
| 69 | + <!-- Reply Section --> | ||
| 70 | + <view v-if="item.reply" class="bg-blue-50 rounded-[16rpx] p-[24rpx]"> | ||
| 71 | + <view class="text-[24rpx] text-gray-500 mb-[8rpx]"> | ||
| 72 | + 客服回复:{{ item.reply_time || '' }} | ||
| 73 | + </view> | ||
| 74 | + <view class="text-[28rpx] text-gray-900 leading-relaxed"> | ||
| 75 | + {{ item.reply }} | ||
| 76 | + </view> | ||
| 77 | + </view> | ||
| 78 | + </view> | ||
| 79 | + </view> | ||
| 80 | + | ||
| 81 | + <!-- Load More --> | ||
| 82 | + <view v-if="hasMore && !loading" class="flex justify-center mt-[40rpx]"> | ||
| 83 | + <nut-button type="default" size="small" @click="loadMore"> | ||
| 84 | + 加载更多 | ||
| 85 | + </nut-button> | ||
| 86 | + </view> | ||
| 87 | + </view> | ||
| 88 | + </scroll-view> | ||
| 89 | + | ||
| 90 | + <!-- Fixed Bottom Button --> | ||
| 91 | + <view class="fixed bottom-0 left-0 right-0 p-[32rpx] bg-white border-t border-gray-200"> | ||
| 92 | + <nut-button type="primary" block class="!h-[88rpx] !rounded-[44rpx] !text-[32rpx]" @click="goToFeedback"> | ||
| 93 | + 反馈意见 | ||
| 94 | + </nut-button> | ||
| 95 | + </view> | ||
| 96 | + </view> | ||
| 97 | +</template> | ||
| 98 | + | ||
| 99 | +<script setup> | ||
| 100 | +import { ref, computed, onMounted } from 'vue' | ||
| 101 | +import { useGo } from '@/hooks/useGo' | ||
| 102 | +import NavHeader from '@/components/NavHeader.vue' | ||
| 103 | +import Taro, { useDidShow } from '@tarojs/taro' | ||
| 104 | +import { listAPI } from '@/api/feedback' | ||
| 105 | + | ||
| 106 | +const go = useGo() | ||
| 107 | + | ||
| 108 | +/** @type {import('vue').Ref<number>} 系统信息(用于计算滚动高度) */ | ||
| 109 | +const systemInfo = ref(null) | ||
| 110 | + | ||
| 111 | +/** @type {import('vue').Ref<boolean>} 加载状态 */ | ||
| 112 | +const loading = ref(false) | ||
| 113 | + | ||
| 114 | +/** @type {import('vue').Ref<Array>} 反馈列表 */ | ||
| 115 | +const feedbackList = ref([]) | ||
| 116 | + | ||
| 117 | +/** @type {import('vue').Ref<number>} 当前页码 */ | ||
| 118 | +const currentPage = ref(0) | ||
| 119 | + | ||
| 120 | +/** @type {import('vue').Ref<number>} 每页数量 */ | ||
| 121 | +const pageSize = ref(10) | ||
| 122 | + | ||
| 123 | +/** @type {import('vue').Ref<boolean>} 是否有更多数据 */ | ||
| 124 | +const hasMore = ref(true) | ||
| 125 | + | ||
| 126 | +/** @type {import('vue').ComputedRef<number>} 滚动区域高度 */ | ||
| 127 | +const scrollHeight = computed(() => { | ||
| 128 | + if (!systemInfo.value) return 500 | ||
| 129 | + | ||
| 130 | + // 导航栏高度 + 状态栏高度 + 底部按钮高度 + padding | ||
| 131 | + const navBarHeight = 44 // 导航栏默认高度 | ||
| 132 | + const statusBarHeight = systemInfo.value.statusBarHeight || 0 | ||
| 133 | + const bottomHeight = 88 + 32 // 按钮高度 + padding | ||
| 134 | + | ||
| 135 | + return systemInfo.value.windowHeight - bottomHeight | ||
| 136 | +}) | ||
| 137 | + | ||
| 138 | +/** | ||
| 139 | + * @description 获取反馈类型标签 | ||
| 140 | + * @param {string} category 类别值:1=功能建议, 3=问题反馈, 7=其他问题 | ||
| 141 | + * @returns {string} 类别标签 | ||
| 142 | + */ | ||
| 143 | +const getTypeLabel = (category) => { | ||
| 144 | + const map = { | ||
| 145 | + '1': '功能建议', | ||
| 146 | + '3': '问题反馈', | ||
| 147 | + '7': '其他问题' | ||
| 148 | + } | ||
| 149 | + return map[category] || '其他' | ||
| 150 | +} | ||
| 151 | + | ||
| 152 | +/** | ||
| 153 | + * @description 获取反馈类型样式类 | ||
| 154 | + * @param {string} category 类别值 | ||
| 155 | + * @returns {string} 样式类名 | ||
| 156 | + */ | ||
| 157 | +const getTypeClass = (category) => { | ||
| 158 | + const map = { | ||
| 159 | + '1': 'bg-blue-100 text-blue-600', | ||
| 160 | + '3': 'bg-red-100 text-red-600', | ||
| 161 | + '7': 'bg-gray-100 text-gray-600' | ||
| 162 | + } | ||
| 163 | + return map[category] || 'bg-gray-100 text-gray-600' | ||
| 164 | +} | ||
| 165 | + | ||
| 166 | +/** | ||
| 167 | + * @description 预览图片 | ||
| 168 | + * @param {Array<string>} urls 图片 URL 列表 | ||
| 169 | + * @param {number} current 当前图片索引 | ||
| 170 | + */ | ||
| 171 | +const previewImage = (urls, current) => { | ||
| 172 | + Taro.previewImage({ | ||
| 173 | + current: urls[current], | ||
| 174 | + urls: urls | ||
| 175 | + }) | ||
| 176 | +} | ||
| 177 | + | ||
| 178 | +/** | ||
| 179 | + * @description 加载反馈列表 | ||
| 180 | + * @param {boolean} isLoadMore 是否为加载更多 | ||
| 181 | + */ | ||
| 182 | +const loadFeedbackList = async (isLoadMore = false) => { | ||
| 183 | + if (loading.value) return | ||
| 184 | + | ||
| 185 | + loading.value = true | ||
| 186 | + | ||
| 187 | + try { | ||
| 188 | + const res = await listAPI({ | ||
| 189 | + page: currentPage.value, | ||
| 190 | + limit: pageSize.value | ||
| 191 | + }) | ||
| 192 | + | ||
| 193 | + if (res.code === 1) { | ||
| 194 | + const newList = res.data.list || [] | ||
| 195 | + | ||
| 196 | + if (isLoadMore) { | ||
| 197 | + feedbackList.value.push(...newList) | ||
| 198 | + } else { | ||
| 199 | + feedbackList.value = newList | ||
| 200 | + } | ||
| 201 | + | ||
| 202 | + // 判断是否还有更多数据 | ||
| 203 | + hasMore.value = newList.length >= pageSize.value | ||
| 204 | + } else { | ||
| 205 | + Taro.showToast({ title: res.msg || '加载失败', icon: 'none' }) | ||
| 206 | + } | ||
| 207 | + } catch (err) { | ||
| 208 | + console.error('加载反馈列表失败:', err) | ||
| 209 | + Taro.showToast({ title: '网络异常,请重试', icon: 'none' }) | ||
| 210 | + } finally { | ||
| 211 | + loading.value = false | ||
| 212 | + } | ||
| 213 | +} | ||
| 214 | + | ||
| 215 | +/** | ||
| 216 | + * @description 加载更多 | ||
| 217 | + */ | ||
| 218 | +const loadMore = () => { | ||
| 219 | + if (!hasMore.value || loading.value) return | ||
| 220 | + currentPage.value++ | ||
| 221 | + loadFeedbackList(true) | ||
| 222 | +} | ||
| 223 | + | ||
| 224 | +/** | ||
| 225 | + * @description 滚动到底部时自动加载 | ||
| 226 | + */ | ||
| 227 | +const onScrollToLower = () => { | ||
| 228 | + if (hasMore.value && !loading.value) { | ||
| 229 | + loadMore() | ||
| 230 | + } | ||
| 231 | +} | ||
| 232 | + | ||
| 233 | +/** | ||
| 234 | + * @description 跳转到反馈提交页面 | ||
| 235 | + */ | ||
| 236 | +const goToFeedback = () => { | ||
| 237 | + go('/pages/feedback/index') | ||
| 238 | +} | ||
| 239 | + | ||
| 240 | +/** | ||
| 241 | + * @description 页面首次加载时获取系统信息 | ||
| 242 | + */ | ||
| 243 | +onMounted(() => { | ||
| 244 | + // 获取系统信息 | ||
| 245 | + Taro.getSystemInfo({ | ||
| 246 | + success: (res) => { | ||
| 247 | + systemInfo.value = res | ||
| 248 | + }, | ||
| 249 | + fail: () => { | ||
| 250 | + // 使用默认值 | ||
| 251 | + systemInfo.value = { | ||
| 252 | + windowHeight: 667, | ||
| 253 | + statusBarHeight: 44 | ||
| 254 | + } | ||
| 255 | + } | ||
| 256 | + }) | ||
| 257 | +}) | ||
| 258 | + | ||
| 259 | +/** | ||
| 260 | + * @description 页面显示时刷新列表(从提交页返回时也会触发) | ||
| 261 | + */ | ||
| 262 | +useDidShow(() => { | ||
| 263 | + // 重置为第一页 | ||
| 264 | + currentPage.value = 0 | ||
| 265 | + feedbackList.value = [] | ||
| 266 | + | ||
| 267 | + // 加载反馈列表 | ||
| 268 | + loadFeedbackList() | ||
| 269 | +}) | ||
| 270 | +</script> | ||
| 271 | + | ||
| 272 | +<style lang="less"> | ||
| 273 | +.feedback-scroll { | ||
| 274 | + box-sizing: border-box; | ||
| 275 | +} | ||
| 276 | + | ||
| 277 | +.space-y-\[24rpx\] > * + * { | ||
| 278 | + margin-top: 24rpx; | ||
| 279 | +} | ||
| 280 | + | ||
| 281 | +.loading-spinner { | ||
| 282 | + width: 40rpx; | ||
| 283 | + height: 40rpx; | ||
| 284 | + border: 4rpx solid #f3f3f3; | ||
| 285 | + border-top: 4rpx solid #3498db; | ||
| 286 | + border-radius: 50%; | ||
| 287 | + animation: spin 1s linear infinite; | ||
| 288 | +} | ||
| 289 | + | ||
| 290 | +@keyframes spin { | ||
| 291 | + 0% { transform: rotate(0deg); } | ||
| 292 | + 100% { transform: rotate(360deg); } | ||
| 293 | +} | ||
| 294 | +</style> |
| ... | @@ -3,7 +3,7 @@ | ... | @@ -3,7 +3,7 @@ |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | * @LastEditTime: 2026-01-31 14:40:32 | 4 | * @LastEditTime: 2026-01-31 14:40:32 |
| 5 | * @FilePath: /manulife-weapp/src/pages/feedback/index.vue | 5 | * @FilePath: /manulife-weapp/src/pages/feedback/index.vue |
| 6 | - * @Description: 文件描述 | 6 | + * @Description: 意见反馈页面 |
| 7 | --> | 7 | --> |
| 8 | <template> | 8 | <template> |
| 9 | <view class="min-h-screen bg-gray-50 pb-[200rpx]"> | 9 | <view class="min-h-screen bg-gray-50 pb-[200rpx]"> |
| ... | @@ -45,12 +45,33 @@ | ... | @@ -45,12 +45,33 @@ |
| 45 | <!-- Screenshots --> | 45 | <!-- Screenshots --> |
| 46 | <view class="bg-white rounded-[24rpx] p-[32rpx] mb-[40rpx] shadow-sm"> | 46 | <view class="bg-white rounded-[24rpx] p-[32rpx] mb-[40rpx] shadow-sm"> |
| 47 | <view class="text-[30rpx] font-bold text-gray-900 mb-[24rpx]">添加截图(可选)</view> | 47 | <view class="text-[30rpx] font-bold text-gray-900 mb-[24rpx]">添加截图(可选)</view> |
| 48 | - <nut-uploader | 48 | + <view class="flex gap-[24rpx]"> |
| 49 | - url="https://xxx" | 49 | + <!-- Uploaded Images --> |
| 50 | - v-model:file-list="fileList" | 50 | + <view |
| 51 | - maximum="3" | 51 | + v-for="(img, index) in uploadedImages" |
| 52 | - > | 52 | + :key="index" |
| 53 | - </nut-uploader> | 53 | + class="relative w-[160rpx] h-[160rpx] rounded-[16rpx] overflow-hidden" |
| 54 | + > | ||
| 55 | + <image :src="img" mode="aspectFill" class="w-full h-full" /> | ||
| 56 | + <view | ||
| 57 | + class="absolute top-0 right-0 bg-red-500 text-white w-[40rpx] h-[40rpx] flex items-center justify-center" | ||
| 58 | + @tap="removeImage(index)" | ||
| 59 | + > | ||
| 60 | + <text class="text-[24rpx]">×</text> | ||
| 61 | + </view> | ||
| 62 | + </view> | ||
| 63 | + | ||
| 64 | + <!-- Upload Button --> | ||
| 65 | + <view | ||
| 66 | + v-if="uploadedImages.length < 3" | ||
| 67 | + class="w-[160rpx] h-[160rpx] rounded-[16rpx] border-2 border-dashed border-gray-300 flex flex-col items-center justify-center" | ||
| 68 | + @tap="chooseImage" | ||
| 69 | + > | ||
| 70 | + <text class="text-[48rpx] text-gray-400">+</text> | ||
| 71 | + <text class="text-[24rpx] text-gray-400 mt-[8rpx]">上传图片</text> | ||
| 72 | + </view> | ||
| 73 | + </view> | ||
| 74 | + <view class="text-[22rpx] text-gray-400 mt-[16rpx]">最多上传3张图片,每张不超过5MB</view> | ||
| 54 | </view> | 75 | </view> |
| 55 | 76 | ||
| 56 | <!-- Submit Button --> | 77 | <!-- Submit Button --> |
| ... | @@ -67,40 +88,147 @@ import { ref } from 'vue' | ... | @@ -67,40 +88,147 @@ import { ref } from 'vue' |
| 67 | import TabBar from '@/components/TabBar.vue' | 88 | import TabBar from '@/components/TabBar.vue' |
| 68 | import NavHeader from '@/components/NavHeader.vue' | 89 | import NavHeader from '@/components/NavHeader.vue' |
| 69 | import Taro from '@tarojs/taro' | 90 | import Taro from '@tarojs/taro' |
| 91 | +import { addAPI } from '@/api/feedback' | ||
| 92 | +import BASE_URL from '@/utils/config' | ||
| 70 | 93 | ||
| 71 | -const selectedType = ref('suggestion') | 94 | +/** |
| 72 | -const description = ref('') | 95 | + * 反馈类型选项(对应后端 category 值) |
| 73 | -const fileList = ref([]) | 96 | + * 1=功能建议, 3=问题反馈, 7=其他问题 |
| 74 | - | 97 | + * @type {Array<{label: string, value: string}>} |
| 98 | + */ | ||
| 75 | const types = [ | 99 | const types = [ |
| 76 | - { label: '功能建议', value: 'suggestion' }, | 100 | + { label: '功能建议', value: '1' }, |
| 77 | - { label: '问题反馈', value: 'issue' }, | 101 | + { label: '问题反馈', value: '3' }, |
| 78 | - { label: '其他', value: 'other' } | 102 | + { label: '其他', value: '7' } |
| 79 | ] | 103 | ] |
| 80 | 104 | ||
| 81 | -const onSubmit = () => { | 105 | +/** @type {import('vue').Ref<string>} 选中的反馈类型 */ |
| 106 | +const selectedType = ref('1') | ||
| 107 | + | ||
| 108 | +/** @type {import('vue').Ref<string>} 问题描述 */ | ||
| 109 | +const description = ref('') | ||
| 110 | + | ||
| 111 | +/** @type {import('vue').Ref<Array<string>>} 已上传的图片 URL 列表 */ | ||
| 112 | +const uploadedImages = ref([]) | ||
| 113 | + | ||
| 114 | +/** | ||
| 115 | + * @description 选择图片并上传(参考头像上传逻辑) | ||
| 116 | + */ | ||
| 117 | +const chooseImage = () => { | ||
| 118 | + Taro.chooseImage({ | ||
| 119 | + count: 3 - uploadedImages.value.length, // 剩余可上传数量 | ||
| 120 | + sizeType: ['compressed'], // 使用压缩图 | ||
| 121 | + sourceType: ['album', 'camera'], | ||
| 122 | + success: async (res) => { | ||
| 123 | + const tempFile = res.tempFiles[0] | ||
| 124 | + | ||
| 125 | + // 检查文件大小(5MB限制) | ||
| 126 | + if (tempFile.size > 5 * 1024 * 1024) { | ||
| 127 | + Taro.showToast({ | ||
| 128 | + title: '图片大小不能超过5MB', | ||
| 129 | + icon: 'none', | ||
| 130 | + }) | ||
| 131 | + return | ||
| 132 | + } | ||
| 133 | + | ||
| 134 | + // 显示上传进度 | ||
| 135 | + Taro.showLoading({ title: '上传中...', mask: true }) | ||
| 136 | + | ||
| 137 | + // 使用 Taro.uploadFile 上传到服务器(参考头像上传) | ||
| 138 | + Taro.uploadFile({ | ||
| 139 | + url: BASE_URL + '/admin/?m=srv&a=upload&image_audit=1', | ||
| 140 | + filePath: tempFile.path, | ||
| 141 | + name: 'file', | ||
| 142 | + success: (uploadRes) => { | ||
| 143 | + Taro.hideLoading() | ||
| 144 | + | ||
| 145 | + const data = JSON.parse(uploadRes.data) | ||
| 146 | + | ||
| 147 | + // 检查是否为审核不通过 | ||
| 148 | + if (data.data && data.data.audit_code == -1) { | ||
| 149 | + Taro.showModal({ | ||
| 150 | + title: '温馨提示', | ||
| 151 | + content: data.msg || '图片审核不通过', | ||
| 152 | + showCancel: false | ||
| 153 | + }) | ||
| 154 | + return | ||
| 155 | + } | ||
| 156 | + | ||
| 157 | + if (data.code === 0) { // 注意:后端 code=0 表示成功 | ||
| 158 | + uploadedImages.value.push(data.data.src) | ||
| 159 | + Taro.showToast({ | ||
| 160 | + title: '上传成功', | ||
| 161 | + icon: 'success' | ||
| 162 | + }) | ||
| 163 | + } else { | ||
| 164 | + Taro.showToast({ | ||
| 165 | + title: data.msg || '上传失败', | ||
| 166 | + icon: 'none' | ||
| 167 | + }) | ||
| 168 | + } | ||
| 169 | + }, | ||
| 170 | + fail: () => { | ||
| 171 | + Taro.hideLoading() | ||
| 172 | + Taro.showToast({ | ||
| 173 | + title: '上传失败,请稍后重试', | ||
| 174 | + icon: 'none' | ||
| 175 | + }) | ||
| 176 | + } | ||
| 177 | + }) | ||
| 178 | + } | ||
| 179 | + }) | ||
| 180 | +} | ||
| 181 | + | ||
| 182 | +/** | ||
| 183 | + * @description 移除图片 | ||
| 184 | + * @param {number} index 图片索引 | ||
| 185 | + */ | ||
| 186 | +const removeImage = (index) => { | ||
| 187 | + uploadedImages.value.splice(index, 1) | ||
| 188 | +} | ||
| 189 | + | ||
| 190 | +/** | ||
| 191 | + * @description 提交意见反馈 | ||
| 192 | + */ | ||
| 193 | +const onSubmit = async () => { | ||
| 194 | + // 验证必填项 | ||
| 82 | if (!description.value) { | 195 | if (!description.value) { |
| 83 | Taro.showToast({ title: '请输入问题描述', icon: 'none' }) | 196 | Taro.showToast({ title: '请输入问题描述', icon: 'none' }) |
| 84 | return | 197 | return |
| 85 | } | 198 | } |
| 86 | 199 | ||
| 87 | - Taro.showLoading({ title: '提交中...' }) | 200 | + Taro.showLoading({ title: '提交中...', mask: true }) |
| 201 | + | ||
| 202 | + try { | ||
| 203 | + // 调用 API 提交反馈 | ||
| 204 | + const res = await addAPI({ | ||
| 205 | + category: selectedType.value, // 反馈类别:1=功能建议, 3=问题反馈, 7=其他问题 | ||
| 206 | + note: description.value, // 反馈内容 | ||
| 207 | + images: uploadedImages.value // 图片 URL 数组(可选) | ||
| 208 | + }) | ||
| 88 | 209 | ||
| 89 | - // Simulate API call | ||
| 90 | - setTimeout(() => { | ||
| 91 | Taro.hideLoading() | 210 | Taro.hideLoading() |
| 92 | - Taro.showToast({ title: '提交成功', icon: 'success' }) | ||
| 93 | 211 | ||
| 94 | - // Reset form | 212 | + if (res.code === 1) { |
| 95 | - description.value = '' | 213 | + Taro.showToast({ title: '提交成功', icon: 'success' }) |
| 96 | - fileList.value = [] | ||
| 97 | - selectedType.value = 'suggestion' | ||
| 98 | 214 | ||
| 99 | - // Navigate back or stay | 215 | + // 重置表单 |
| 100 | - setTimeout(() => { | 216 | + description.value = '' |
| 217 | + uploadedImages.value = [] | ||
| 218 | + selectedType.value = '1' | ||
| 219 | + | ||
| 220 | + // 延迟返回 | ||
| 221 | + setTimeout(() => { | ||
| 101 | Taro.navigateBack() | 222 | Taro.navigateBack() |
| 102 | - }, 1500) | 223 | + }, 1500) |
| 103 | - }, 1000) | 224 | + } else { |
| 225 | + Taro.showToast({ title: res.msg || '提交失败', icon: 'none' }) | ||
| 226 | + } | ||
| 227 | + } catch (err) { | ||
| 228 | + Taro.hideLoading() | ||
| 229 | + console.error('提交反馈失败:', err) | ||
| 230 | + Taro.showToast({ title: '网络异常,请重试', icon: 'none' }) | ||
| 231 | + } | ||
| 104 | } | 232 | } |
| 105 | </script> | 233 | </script> |
| 106 | 234 | ... | ... |
| ... | @@ -123,7 +123,7 @@ const menuItems = [ | ... | @@ -123,7 +123,7 @@ const menuItems = [ |
| 123 | { title: '我的计划书', icon: 'order', path: '/pages/plan/index' }, | 123 | { title: '我的计划书', icon: 'order', path: '/pages/plan/index' }, |
| 124 | { title: '我的收藏', icon: 'star', path: '/pages/favorites/index' }, | 124 | { title: '我的收藏', icon: 'star', path: '/pages/favorites/index' }, |
| 125 | { title: '帮助中心', icon: 'service', path: '/pages/help-center/index' }, | 125 | { title: '帮助中心', icon: 'service', path: '/pages/help-center/index' }, |
| 126 | - { title: '意见反馈', icon: 'edit', path: '/pages/feedback/index' } | 126 | + { title: '意见反馈', icon: 'edit', path: '/pages/feedback-list/index' } |
| 127 | ] | 127 | ] |
| 128 | 128 | ||
| 129 | const handleMenuClick = (item) => { | 129 | const handleMenuClick = (item) => { | ... | ... |
-
Please register or login to post a comment