complaint.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. <template>
  2. <view class="acct-pwd-page">
  3. <view class="acct-pwd-nav" :style="{ paddingTop: statusBar + 'px' }">
  4. <view class="acct-pwd-nav-row">
  5. <view class="acct-pwd-back" @tap="goBack">
  6. <text class="acct-pwd-back-icon">←</text>
  7. </view>
  8. <text class="acct-pwd-nav-title">{{ copy.pageTitle }}</text>
  9. <view class="acct-pwd-nav-placeholder" />
  10. </view>
  11. </view>
  12. <scroll-view
  13. scroll-y
  14. class="acct-pwd-scroll"
  15. :style="{ height: scrollHeight + 'px' }"
  16. >
  17. <view class="acct-pwd-body" :style="{ paddingBottom: footerPad + 'px' }">
  18. <view class="acct-pwd-hero">
  19. <view class="acct-pwd-hero-icon-wrap">
  20. <text class="acct-pwd-hero-icon">🚩</text>
  21. </view>
  22. <text class="acct-pwd-hero-title">{{ copy.heroTitle }}</text>
  23. <text class="acct-pwd-hero-desc">{{ heroDesc }}</text>
  24. </view>
  25. <text class="cp-section">{{ copy.sectionReason }}</text>
  26. <view class="acct-pwd-card cp-picker-card">
  27. <picker
  28. mode="selector"
  29. :range="reasonLabels"
  30. :value="reasonIndex >= 0 ? reasonIndex : 0"
  31. :disabled="!reasonLabels.length"
  32. @change="onReasonPick"
  33. >
  34. <view class="cp-picker-row">
  35. <text
  36. class="cp-picker-value"
  37. :class="{ 'cp-picker-value--placeholder': reasonIndex < 0 }"
  38. >{{ reasonLabel }}</text>
  39. <text class="cp-picker-chevron">›</text>
  40. </view>
  41. </picker>
  42. </view>
  43. <text class="cp-section">{{ copy.sectionScreenshots }}</text>
  44. <view class="acct-pwd-card">
  45. <view class="cp-img-head">
  46. <text class="cp-img-head-label">{{ copy.previewImages }}</text>
  47. <text class="cp-img-head-count">{{ form.images.length }}/8</text>
  48. </view>
  49. <view class="cp-img-grid">
  50. <view
  51. v-for="(image, index) in form.images"
  52. :key="index"
  53. class="cp-img-cell"
  54. >
  55. <image
  56. class="cp-img-thumb"
  57. :src="thumb(image)"
  58. mode="aspectFill"
  59. @tap="previewImages(index)"
  60. />
  61. <view class="cp-img-remove" @tap.stop="removeImage(index)">
  62. <text class="cp-img-remove-icon">×</text>
  63. </view>
  64. </view>
  65. <view
  66. v-if="form.images.length < 8"
  67. class="cp-img-cell cp-img-cell--add"
  68. @tap="pickImages"
  69. >
  70. <text class="cp-img-add-icon">+</text>
  71. <text class="cp-img-add-txt">{{ copy.addImage }}</text>
  72. </view>
  73. </view>
  74. </view>
  75. <text class="cp-section">{{ copy.sectionDetails }}</text>
  76. <view class="acct-pwd-card">
  77. <textarea
  78. v-model="form.content"
  79. class="cp-textarea"
  80. maxlength="-1"
  81. :placeholder="copy.placeholder"
  82. placeholder-class="cp-textarea-placeholder"
  83. />
  84. </view>
  85. </view>
  86. </scroll-view>
  87. <view class="cp-footer" :style="{ paddingBottom: safeBottom + 'px' }">
  88. <button
  89. class="acct-pwd-submit"
  90. :disabled="submitting"
  91. :loading="submitting"
  92. :class="{ 'acct-pwd-submit--busy': submitting }"
  93. @tap="submit"
  94. >
  95. <text class="acct-pwd-submit__text">{{ submitting ? copy.submitting : copy.submit }}</text>
  96. </button>
  97. </view>
  98. </view>
  99. </template>
  100. <script>
  101. import {
  102. buildComplaintDraft,
  103. fetchComplaintReasons,
  104. submitComplaint,
  105. uploadComplaintImage
  106. } from '@/features/account/complaint-gateway'
  107. export default {
  108. data() {
  109. return {
  110. statusBar: 0,
  111. scrollHeight: 600,
  112. safeBottom: 0,
  113. footerPad: 120,
  114. submitting: false,
  115. uploading: false,
  116. typeName: '',
  117. targetId: 0,
  118. reasonIndex: -1,
  119. reasonOptions: [],
  120. form: {
  121. type: 0,
  122. complaint_user_id: 0,
  123. complaint_shop_id: 0,
  124. complaint_goods_id: 0,
  125. images: [],
  126. content: '',
  127. reason: -1
  128. }
  129. }
  130. },
  131. computed: {
  132. copy() {
  133. const t = this.$t.bind(this)
  134. return {
  135. pageTitle: t('user.panelReport'),
  136. heroTitle: t('complaint.heroTitle'),
  137. sectionReason: t('complaint.reason'),
  138. sectionScreenshots: t('complaint.screenshots'),
  139. sectionDetails: t('complaint.details'),
  140. previewImages: t('complaint.preview-images'),
  141. addImage: t('complaint.add-image'),
  142. placeholder: t('complaint.placeholder'),
  143. submit: t('complaint.submit'),
  144. submitting: t('complaint.submitting'),
  145. selectReason: t('complaint.select-reason'),
  146. reasonRequired: t('complaint.reason-required'),
  147. contentRequired: t('complaint.content-required'),
  148. submitOk: t('complaint.submit-ok'),
  149. network: t('user.realnameNetworkError')
  150. }
  151. },
  152. heroDesc() {
  153. if (this.targetId) {
  154. return this.$t('complaint.targetHint', {
  155. typeName: this.typeName,
  156. id: this.targetId
  157. })
  158. }
  159. return this.$t('complaint.generalHint')
  160. },
  161. reasonLabels() {
  162. return this.reasonOptions.map((item) => item.title)
  163. },
  164. reasonLabel() {
  165. if (this.reasonIndex < 0 || !this.reasonOptions[this.reasonIndex]) {
  166. return this.copy.selectReason
  167. }
  168. return this.reasonOptions[this.reasonIndex].title
  169. }
  170. },
  171. onLoad(option) {
  172. const sys = uni.getSystemInfoSync()
  173. this.statusBar = sys.statusBarHeight || 0
  174. this.safeBottom = sys.safeAreaInsets ? sys.safeAreaInsets.bottom : 0
  175. const navH = this.statusBar + uni.upx2px(88)
  176. this.scrollHeight = (sys.windowHeight || 667) - navH - uni.upx2px(120)
  177. this.footerPad = uni.upx2px(120) + this.safeBottom
  178. const built = buildComplaintDraft(option || {}, this.$t.bind(this))
  179. this.form = { ...built.draft }
  180. this.typeName = built.typeName
  181. this.targetId = built.targetId
  182. this.loadReasons()
  183. },
  184. methods: {
  185. goBack() {
  186. if (getCurrentPages().length > 1) {
  187. uni.navigateBack({ delta: 1 })
  188. } else {
  189. uni.switchTab({
  190. url: '/pages/mine/index',
  191. fail: () => uni.reLaunch({ url: '/pages/mine/index' })
  192. })
  193. }
  194. },
  195. thumb(url) {
  196. return this.$appKit && this.$appKit.oss ? this.$appKit.oss(url, 200, 200) : url
  197. },
  198. async loadReasons() {
  199. try {
  200. const list = await fetchComplaintReasons()
  201. if (list.length) {
  202. this.reasonOptions = list
  203. }
  204. } catch (_) {}
  205. },
  206. onReasonPick(e) {
  207. const idx = Number(e.detail.value)
  208. if (Number.isNaN(idx) || idx < 0) {
  209. this.reasonIndex = -1
  210. this.form.reason = -1
  211. return
  212. }
  213. this.reasonIndex = idx
  214. const row = this.reasonOptions[idx]
  215. this.form.reason = row ? row.code_num : idx
  216. },
  217. pickImages() {
  218. if (this.uploading) return
  219. const remain = 8 - this.form.images.length
  220. if (remain <= 0) return
  221. uni.chooseImage({
  222. sourceType: ['camera', 'album'],
  223. sizeType: ['compressed'],
  224. count: remain,
  225. success: (res) => {
  226. this.uploadChosen(res.tempFilePaths || [])
  227. }
  228. })
  229. },
  230. async uploadChosen(paths) {
  231. if (!paths.length) return
  232. this.uploading = true
  233. uni.showLoading({ mask: true, title: this.$t('complaint.uploading') })
  234. try {
  235. for (const path of paths) {
  236. const url = await uploadComplaintImage(path)
  237. if (url) this.form.images.push(url)
  238. }
  239. } catch (err) {
  240. this.$appKit.msg((err && err.message) || this.copy.network)
  241. } finally {
  242. this.uploading = false
  243. uni.hideLoading()
  244. }
  245. },
  246. removeImage(index) {
  247. this.form.images.splice(index, 1)
  248. },
  249. previewImages(index) {
  250. const urls = this.form.images.map((item) =>
  251. this.$appKit.oss ? this.$appKit.oss(item, 500) : item
  252. )
  253. uni.previewImage({
  254. urls,
  255. current: urls[index],
  256. indicator: 'number'
  257. })
  258. },
  259. async submit() {
  260. if (this.submitting) return
  261. if (this.form.reason === -1) {
  262. this.$appKit.msg(this.copy.reasonRequired)
  263. return
  264. }
  265. if (!String(this.form.content || '').trim()) {
  266. this.$appKit.msg(this.copy.contentRequired)
  267. return
  268. }
  269. this.submitting = true
  270. uni.showLoading({ mask: true })
  271. try {
  272. await submitComplaint({ ...this.form })
  273. this.$appKit.msg(this.copy.submitOk)
  274. setTimeout(() => {
  275. uni.navigateTo({
  276. url: '/pages/account/complaint-lists',
  277. fail: () => this.goBack()
  278. })
  279. }, 400)
  280. } catch (err) {
  281. this.$appKit.msg((err && err.message) || this.copy.network)
  282. } finally {
  283. this.submitting = false
  284. uni.hideLoading()
  285. }
  286. }
  287. }
  288. }
  289. </script>
  290. <style scoped>
  291. @import './account-flow.css';
  292. .cp-section {
  293. display: block;
  294. padding: 0 8rpx 16rpx;
  295. font-size: 26rpx;
  296. color: #8a8d9e;
  297. }
  298. .cp-picker-card {
  299. padding: 0 28rpx;
  300. }
  301. .cp-picker-row {
  302. display: flex;
  303. flex-direction: row;
  304. align-items: center;
  305. justify-content: space-between;
  306. min-height: 96rpx;
  307. }
  308. .cp-picker-value {
  309. flex: 1;
  310. font-size: 30rpx;
  311. color: #1a1d26;
  312. }
  313. .cp-picker-value--placeholder {
  314. color: #b8bcc8;
  315. }
  316. .cp-picker-chevron {
  317. font-size: 36rpx;
  318. color: #c5cad6;
  319. line-height: 1;
  320. }
  321. .cp-img-head {
  322. display: flex;
  323. flex-direction: row;
  324. align-items: center;
  325. justify-content: space-between;
  326. margin-bottom: 20rpx;
  327. }
  328. .cp-img-head-label {
  329. font-size: 28rpx;
  330. color: #1a1d26;
  331. font-weight: 500;
  332. }
  333. .cp-img-head-count {
  334. font-size: 24rpx;
  335. color: #8a8d9e;
  336. }
  337. .cp-img-grid {
  338. display: flex;
  339. flex-direction: row;
  340. flex-wrap: wrap;
  341. margin: -8rpx;
  342. }
  343. .cp-img-cell {
  344. position: relative;
  345. width: calc(25% - 16rpx);
  346. height: 152rpx;
  347. margin: 8rpx;
  348. border-radius: 16rpx;
  349. overflow: hidden;
  350. background-color: #f0f3f8;
  351. }
  352. .cp-img-cell--add {
  353. display: flex;
  354. flex-direction: column;
  355. align-items: center;
  356. justify-content: center;
  357. border: 2rpx dashed #d5dae6;
  358. background-color: #f7f9fc;
  359. }
  360. .cp-img-thumb {
  361. width: 100%;
  362. height: 100%;
  363. }
  364. .cp-img-remove {
  365. position: absolute;
  366. top: 0;
  367. right: 0;
  368. width: 40rpx;
  369. height: 40rpx;
  370. background: rgba(239, 68, 68, 0.92);
  371. display: flex;
  372. align-items: center;
  373. justify-content: center;
  374. border-bottom-left-radius: 12rpx;
  375. }
  376. .cp-img-remove-icon {
  377. color: #ffffff;
  378. font-size: 28rpx;
  379. line-height: 1;
  380. }
  381. .cp-img-add-icon {
  382. font-size: 44rpx;
  383. color: #4a8cff;
  384. line-height: 1;
  385. }
  386. .cp-img-add-txt {
  387. margin-top: 6rpx;
  388. font-size: 20rpx;
  389. color: #8a8d9e;
  390. }
  391. .cp-textarea {
  392. width: 100%;
  393. min-height: 220rpx;
  394. font-size: 28rpx;
  395. color: #1a1d26;
  396. line-height: 1.5;
  397. }
  398. .cp-textarea-placeholder {
  399. color: #b8bcc8;
  400. }
  401. .cp-footer {
  402. position: fixed;
  403. left: 0;
  404. right: 0;
  405. bottom: 0;
  406. padding: 16rpx 28rpx 0;
  407. background: linear-gradient(180deg, rgba(244, 246, 251, 0) 0%, #f4f6fb 28%);
  408. box-sizing: border-box;
  409. }
  410. </style>