Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

322 строки
8.5 KiB

  1. <template>
  2. <view class="wrap">
  3. <view v-if="tipText" class="tip-card">{{ tipText }}</view>
  4. <!-- 反馈类型 -->
  5. <view class="fb-card">
  6. <text class="fb-card-title"><text class="required">*</text>问题类型</text>
  7. <view class="type-grid">
  8. <view
  9. v-for="item in TYPES"
  10. :key="item.value"
  11. class="type-item"
  12. :class="{ active: form.type === item.value }"
  13. @click="form.type = item.value"
  14. >
  15. {{ item.label }}
  16. </view>
  17. </view>
  18. </view>
  19. <!-- 反馈内容 -->
  20. <view class="fb-card">
  21. <view class="fb-card-head">
  22. <text class="fb-card-title"><text class="required">*</text>反馈内容</text>
  23. <text class="fb-card-count">{{ form.content.length }}/500</text>
  24. </view>
  25. <textarea
  26. v-model="form.content"
  27. class="textarea"
  28. maxlength="500"
  29. placeholder="请描述你遇到的问题或建议,越具体我们越容易定位"
  30. />
  31. </view>
  32. <!-- 联系电话 -->
  33. <view class="fb-card">
  34. <text class="fb-card-title"><text class="required">*</text>联系电话</text>
  35. <input
  36. v-model="form.phone"
  37. class="input"
  38. type="number"
  39. maxlength="11"
  40. placeholder="方便我们回复你的处理结果"
  41. />
  42. </view>
  43. <!-- 截图 -->
  44. <view class="fb-card">
  45. <view class="fb-card-head">
  46. <text class="fb-card-title"><text class="required">*</text>问题截图</text>
  47. <text class="fb-card-count">{{ form.imgs.length }}/9</text>
  48. </view>
  49. <view class="img-grid">
  50. <view v-for="(url, index) in form.imgs" :key="url" class="img-item">
  51. <image class="img" :src="url" mode="aspectFill" @click="previewImg(index)" />
  52. <view class="img-del" @click.stop="removeImg(index)">×</view>
  53. </view>
  54. <view v-if="form.imgs.length < 9" class="img-add" @click="chooseImage">
  55. <text class="add-plus">+</text>
  56. <text class="add-text">上传截图</text>
  57. </view>
  58. </view>
  59. <text class="fb-card-desc">支持 PNG / JPG,单张不超过 10M</text>
  60. </view>
  61. <view class="bottom-bar">
  62. <view class="submit-btn" :class="{ disabled: submitting }" @click="submit">
  63. {{ submitting ? '提交中...' : '提交反馈' }}
  64. </view>
  65. </view>
  66. </view>
  67. </template>
  68. <script setup>
  69. import { reactive, ref } from 'vue'
  70. import { onLoad } from '@dcloudio/uni-app'
  71. import { commonApi } from '@/api/index.js'
  72. import { uploadImage } from '@/utils/upload.js'
  73. import { useUserStore } from '@/store/user.js'
  74. import { useApp } from '@/utils/useApp.js'
  75. const userStore = useUserStore()
  76. const { toast } = useApp()
  77. // 与后端 feedback.type 约定一致:1反馈使用问题 2提出产品意见 3其他咨询
  78. const TYPES = [
  79. { value: 1, label: '反馈使用问题' },
  80. { value: 2, label: '提出产品意见' },
  81. { value: 3, label: '其他咨询' }
  82. ]
  83. const CONFIG_KEY = 'feedback_tip_content'
  84. const form = reactive({ type: 1, content: '', phone: '', imgs: [] })
  85. const tipText = ref('')
  86. const submitting = ref(false)
  87. onLoad(() => {
  88. loadTip()
  89. // 注意:/user/getinfo 返回的手机号是脱敏的(138****1234),不能用来预填,让用户自己输
  90. })
  91. async function loadTip() {
  92. try {
  93. const res = await commonApi.getConfig({ keys: CONFIG_KEY })
  94. const rows = Array.isArray(res.list) ? res.list : []
  95. const item = rows.find((it) => it.key === CONFIG_KEY)
  96. if (item && item.value) tipText.value = item.value
  97. } catch (e) {
  98. // 配置缺失就不展示提示条
  99. }
  100. }
  101. function chooseImage() {
  102. if (!ensureLogin()) return
  103. const count = 9 - form.imgs.length
  104. if (count <= 0) return
  105. uni.chooseImage({
  106. count,
  107. sizeType: ['compressed'],
  108. sourceType: ['album', 'camera'],
  109. success: async (res) => {
  110. const files = res.tempFiles || []
  111. const paths = res.tempFilePaths || []
  112. uni.showLoading({ title: '上传中...', mask: true })
  113. try {
  114. for (let i = 0; i < paths.length; i++) {
  115. if (Number((files[i] || {}).size || 0) > 10 * 1024 * 1024) {
  116. toast('单张图片不能超过 10M')
  117. continue
  118. }
  119. const ret = await uploadImage(paths[i], files[i] || {})
  120. const url = (ret.data && ret.data.url) || ''
  121. if (url) form.imgs.push(url)
  122. }
  123. } catch (e) {
  124. toast((e && e.msg) || '图片上传失败')
  125. } finally {
  126. uni.hideLoading()
  127. }
  128. }
  129. })
  130. }
  131. function removeImg(index) {
  132. form.imgs.splice(index, 1)
  133. }
  134. function previewImg(index) {
  135. uni.previewImage({ urls: form.imgs, current: form.imgs[index] })
  136. }
  137. async function submit() {
  138. if (!ensureLogin()) return
  139. if (submitting.value) return
  140. if (!form.type) return toast('请选择问题类型')
  141. if (!form.content.trim()) return toast('请输入反馈内容')
  142. if (!/^1\d{10}$/.test(form.phone)) return toast('请输入正确的手机号')
  143. if (!form.imgs.length) return toast('请上传问题截图')
  144. submitting.value = true
  145. try {
  146. await commonApi.feedback({
  147. type: form.type,
  148. content: form.content.trim(),
  149. phone: form.phone,
  150. imgs: form.imgs.join(',')
  151. })
  152. toast('反馈成功,感谢你的建议')
  153. setTimeout(() => uni.navigateBack(), 1200)
  154. } catch (e) {
  155. toast((e && e.msg) || '反馈失败,请重试')
  156. } finally {
  157. submitting.value = false
  158. }
  159. }
  160. function ensureLogin() {
  161. if (userStore.isLogin) return true
  162. uni.navigateTo({ url: '/pages/login/login' })
  163. return false
  164. }
  165. </script>
  166. <style lang="scss" scoped>
  167. .wrap {
  168. min-height: 100vh;
  169. padding: 24rpx 28rpx calc(200rpx + env(safe-area-inset-bottom));
  170. background: #f3f7fb;
  171. box-sizing: border-box;
  172. }
  173. .tip-card {
  174. padding: 22rpx 24rpx;
  175. border-radius: 20rpx;
  176. color: #b7791f;
  177. background: #fff8e6;
  178. font-size: 24rpx;
  179. line-height: 1.6;
  180. }
  181. .fb-card {
  182. margin-top: 22rpx;
  183. padding: 28rpx;
  184. border-radius: 24rpx;
  185. background: #fff;
  186. }
  187. .fb-card-head { display: flex; align-items: baseline; }
  188. .fb-card-title { display: block; color: #172033; font-size: 30rpx; font-weight: 800; }
  189. .fb-card-count { margin-left: auto; color: #8a95a6; font-size: 23rpx; }
  190. .fb-card-desc { display: block; margin-top: 14rpx; color: #8a95a6; font-size: 22rpx; }
  191. .required { margin-right: 4rpx; color: #ef4444; }
  192. .type-grid {
  193. margin-top: 20rpx;
  194. display: grid;
  195. grid-template-columns: repeat(3, minmax(0, 1fr));
  196. gap: 14rpx;
  197. }
  198. .type-item {
  199. height: 76rpx;
  200. border-radius: 18rpx;
  201. color: #596579;
  202. background: #f7f9fd;
  203. border: 2rpx solid transparent;
  204. font-size: 24rpx;
  205. font-weight: 700;
  206. line-height: 76rpx;
  207. text-align: center;
  208. box-sizing: border-box;
  209. overflow: hidden;
  210. white-space: nowrap;
  211. text-overflow: ellipsis;
  212. }
  213. .type-item.active { color: #1f8cff; background: #eff8ff; border-color: #1f8cff; }
  214. .textarea {
  215. margin-top: 20rpx;
  216. width: 100%;
  217. min-height: 220rpx;
  218. padding: 22rpx;
  219. border-radius: 20rpx;
  220. color: #172033;
  221. background: #f7f9fd;
  222. font-size: 25rpx;
  223. line-height: 1.55;
  224. box-sizing: border-box;
  225. }
  226. .input {
  227. margin-top: 20rpx;
  228. width: 100%;
  229. height: 88rpx;
  230. padding: 0 22rpx;
  231. border-radius: 20rpx;
  232. color: #172033;
  233. background: #f7f9fd;
  234. font-size: 27rpx;
  235. box-sizing: border-box;
  236. }
  237. .img-grid {
  238. margin-top: 20rpx;
  239. display: grid;
  240. grid-template-columns: repeat(4, minmax(0, 1fr));
  241. gap: 14rpx;
  242. }
  243. .img-item, .img-add {
  244. position: relative;
  245. width: 100%;
  246. height: 150rpx;
  247. border-radius: 18rpx;
  248. overflow: hidden;
  249. }
  250. .img { width: 100%; height: 100%; }
  251. .img-del {
  252. position: absolute;
  253. right: 0;
  254. top: 0;
  255. width: 40rpx;
  256. height: 40rpx;
  257. border-radius: 0 0 0 16rpx;
  258. color: #fff;
  259. background: rgba(15, 23, 42, 0.55);
  260. font-size: 26rpx;
  261. line-height: 38rpx;
  262. text-align: center;
  263. }
  264. .img-add {
  265. border: 2rpx dashed rgba(31, 140, 255, 0.35);
  266. background: #f8fbff;
  267. display: flex;
  268. flex-direction: column;
  269. align-items: center;
  270. justify-content: center;
  271. box-sizing: border-box;
  272. }
  273. .add-plus { color: #1f8cff; font-size: 44rpx; line-height: 1; }
  274. .add-text { margin-top: 8rpx; color: #8a95a6; font-size: 21rpx; }
  275. .bottom-bar {
  276. position: fixed;
  277. left: 0;
  278. right: 0;
  279. bottom: 0;
  280. padding: 18rpx 28rpx calc(18rpx + env(safe-area-inset-bottom));
  281. background: rgba(255, 255, 255, 0.96);
  282. box-shadow: 0 -10rpx 30rpx rgba(32, 56, 92, 0.1);
  283. }
  284. .submit-btn {
  285. height: 88rpx;
  286. border-radius: 999rpx;
  287. color: #fff;
  288. background: linear-gradient(135deg, #1f8cff, #41b9ff);
  289. font-size: 30rpx;
  290. font-weight: 800;
  291. line-height: 88rpx;
  292. text-align: center;
  293. box-shadow: 0 12rpx 24rpx rgba(31, 140, 255, 0.28);
  294. }
  295. .submit-btn.disabled { opacity: 0.6; }
  296. </style>