feedback-lists.vue 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. <template>
  2. <view class="cp-list-page">
  3. <view class="cp-list-nav" :style="{ paddingTop: statusBar + 'px' }">
  4. <view class="cp-list-nav-row">
  5. <view class="cp-list-back" @tap="goBack">
  6. <text class="cp-list-back-icon">←</text>
  7. </view>
  8. <text class="cp-list-nav-title">{{ copy.title }}</text>
  9. <view class="cp-list-nav-placeholder" />
  10. </view>
  11. </view>
  12. <scroll-view
  13. scroll-y
  14. class="cp-list-scroll"
  15. :style="{ height: scrollHeight + 'px' }"
  16. :lower-threshold="120"
  17. @scrolltolower="onScrollToLower"
  18. >
  19. <view class="cp-list-body" :style="{ paddingBottom: safeBottom + 'px' }">
  20. <view v-if="loading && !rows.length" class="cp-list-state">
  21. <text class="cp-list-state-txt">{{ copy.loading }}</text>
  22. </view>
  23. <empty
  24. v-else-if="loaded && !rows.length"
  25. :text="copy.empty"
  26. />
  27. <view v-else class="cp-list-stack">
  28. <view
  29. v-for="item in rows"
  30. :key="item.id"
  31. class="cp-list-card"
  32. >
  33. <view class="cp-list-card-head">
  34. <view class="cp-list-user">
  35. <image
  36. class="cp-list-avatar"
  37. :src="avatarUrl"
  38. mode="aspectFill"
  39. />
  40. <view class="cp-list-user-meta">
  41. <text class="cp-list-user-name">{{ nickname }}</text>
  42. <text class="cp-list-user-time">{{ timeText(item.createtime) }}</text>
  43. </view>
  44. </view>
  45. <view v-if="item.score > 0" class="fb-list-stars">
  46. <text
  47. v-for="n in 5"
  48. :key="n"
  49. class="fb-list-star"
  50. :class="{ 'fb-list-star--on': item.score >= n }"
  51. >★</text>
  52. </view>
  53. </view>
  54. <view v-if="item.status_text" class="fb-list-status">
  55. <text class="fb-list-status-tag">{{ item.status_text }}</text>
  56. </view>
  57. <text class="cp-list-content">{{ item.content || copy.noContent }}</text>
  58. <view
  59. v-if="item.images && item.images.length"
  60. class="cp-list-images"
  61. :class="'cp-list-images--' + imageColClass(item.images.length)"
  62. >
  63. <view
  64. v-for="(image, index) in item.images"
  65. :key="index"
  66. class="cp-list-img-cell"
  67. @tap="previewImages(item.images, index)"
  68. >
  69. <image
  70. class="cp-list-img"
  71. :src="imageThumb(image)"
  72. mode="aspectFill"
  73. />
  74. </view>
  75. </view>
  76. <view v-if="item.contact" class="fb-list-contact">
  77. <text class="fb-list-contact-label">{{ copy.contactLabel }}</text>
  78. <text class="fb-list-contact-text">{{ item.contact }}</text>
  79. </view>
  80. </view>
  81. </view>
  82. <uni-load-more
  83. v-if="rows.length"
  84. :status="loadStatus"
  85. :content-text="loadMoreText"
  86. />
  87. </view>
  88. </scroll-view>
  89. </view>
  90. </template>
  91. <script>
  92. import { mapState } from 'vuex'
  93. import { fetchFeedbackListPage } from '@/features/account/feedback-gateway'
  94. export default {
  95. data() {
  96. return {
  97. statusBar: 0,
  98. scrollHeight: 600,
  99. safeBottom: 0,
  100. rows: [],
  101. page: 1,
  102. lastPage: 1,
  103. loading: false,
  104. loaded: false,
  105. loadStatus: 'more'
  106. }
  107. },
  108. computed: {
  109. ...mapState('user', {
  110. profile: (state) => state
  111. }),
  112. copy() {
  113. const t = this.$t.bind(this)
  114. return {
  115. title: t('feedback.myList'),
  116. loading: t('feedback-lists.loading'),
  117. empty: t('feedback.no-feedback-found'),
  118. noContent: t('feedback-lists.no-content'),
  119. contactLabel: t('feedback-lists.contactLabel'),
  120. network: t('user.realnameNetworkError')
  121. }
  122. },
  123. nickname() {
  124. return (this.profile && this.profile.nickname) || this.$t('complaint.guest')
  125. },
  126. avatarUrl() {
  127. const raw = (this.profile && this.profile.avatar) || ''
  128. return this.$appKit && this.$appKit.oss
  129. ? this.$appKit.oss(raw, 72, 72, 2, 'avatar')
  130. : raw
  131. },
  132. loadMoreText() {
  133. return {
  134. contentdown: ' ',
  135. contentrefresh: this.copy.loading,
  136. contentnomore: ''
  137. }
  138. }
  139. },
  140. onLoad() {
  141. const sys = uni.getSystemInfoSync()
  142. this.statusBar = sys.statusBarHeight || 0
  143. this.safeBottom = sys.safeAreaInsets ? sys.safeAreaInsets.bottom : 0
  144. const navH = this.statusBar + uni.upx2px(88)
  145. this.scrollHeight = (sys.windowHeight || 667) - navH
  146. this.reloadList()
  147. },
  148. onPullDownRefresh() {
  149. this.reloadList()
  150. },
  151. methods: {
  152. goBack() {
  153. if (getCurrentPages().length > 1) {
  154. uni.navigateBack({ delta: 1 })
  155. } else {
  156. uni.switchTab({
  157. url: '/pages/mine/index',
  158. fail: () => uni.reLaunch({ url: '/pages/mine/index' })
  159. })
  160. }
  161. },
  162. timeText(ts) {
  163. return this.$appKit && this.$appKit.timeToDate
  164. ? this.$appKit.timeToDate(ts)
  165. : ''
  166. },
  167. imageColClass(count) {
  168. if (count > 3) return '3'
  169. return String(count || 1)
  170. },
  171. imageThumb(url) {
  172. return this.$appKit && this.$appKit.oss ? this.$appKit.oss(url, 248, 0) : url
  173. },
  174. previewImages(images, index) {
  175. const urls = images.map((item) =>
  176. this.$appKit.oss ? this.$appKit.oss(item, 500) : item
  177. )
  178. uni.previewImage({
  179. urls,
  180. current: urls[index],
  181. indicator: 'number'
  182. })
  183. },
  184. reloadList() {
  185. this.page = 1
  186. this.lastPage = 1
  187. this.rows = []
  188. this.loaded = false
  189. this.loadStatus = 'more'
  190. this.loadPage(true)
  191. },
  192. onScrollToLower() {
  193. if (this.loading || this.page >= this.lastPage) {
  194. this.loadStatus = 'noMore'
  195. return
  196. }
  197. this.page += 1
  198. this.loadStatus = 'loading'
  199. this.loadPage(false)
  200. },
  201. async loadPage(replace) {
  202. if (this.loading) return
  203. this.loading = true
  204. try {
  205. const res = await fetchFeedbackListPage(this.page)
  206. this.rows = replace ? res.rows : this.rows.concat(res.rows)
  207. this.page = res.currentPage
  208. this.lastPage = res.lastPage
  209. this.loadStatus = res.total === 0 || this.page >= this.lastPage ? 'noMore' : 'more'
  210. } catch (err) {
  211. this.$appKit.msg((err && err.message) || this.copy.network)
  212. this.loadStatus = 'more'
  213. } finally {
  214. this.loading = false
  215. this.loaded = true
  216. uni.stopPullDownRefresh()
  217. }
  218. }
  219. }
  220. }
  221. </script>
  222. <style scoped>
  223. .cp-list-page {
  224. min-height: 100vh;
  225. background-color: #f4f6fb;
  226. }
  227. .cp-list-nav {
  228. background-color: #ffffff;
  229. border-bottom: 1rpx solid #eef1f6;
  230. }
  231. .cp-list-nav-row {
  232. display: flex;
  233. flex-direction: row;
  234. align-items: center;
  235. height: 88rpx;
  236. padding: 0 24rpx;
  237. box-sizing: border-box;
  238. }
  239. .cp-list-back {
  240. width: 64rpx;
  241. height: 64rpx;
  242. display: flex;
  243. align-items: center;
  244. justify-content: center;
  245. }
  246. .cp-list-back-icon {
  247. font-size: 40rpx;
  248. color: #1a1d26;
  249. line-height: 1;
  250. }
  251. .cp-list-nav-title {
  252. flex: 1;
  253. text-align: center;
  254. font-size: 32rpx;
  255. font-weight: 600;
  256. color: #1a1d26;
  257. }
  258. .cp-list-nav-placeholder {
  259. width: 64rpx;
  260. }
  261. .cp-list-scroll {
  262. width: 100%;
  263. }
  264. .cp-list-body {
  265. padding: 24rpx;
  266. box-sizing: border-box;
  267. }
  268. .cp-list-state {
  269. padding: 80rpx 0;
  270. text-align: center;
  271. }
  272. .cp-list-state-txt {
  273. font-size: 28rpx;
  274. color: #8a8d9e;
  275. }
  276. .cp-list-stack {
  277. display: flex;
  278. flex-direction: column;
  279. gap: 24rpx;
  280. }
  281. .cp-list-card {
  282. background-color: #ffffff;
  283. border-radius: 24rpx;
  284. padding: 28rpx;
  285. box-shadow: 0 8rpx 32rpx rgba(26, 29, 38, 0.04);
  286. }
  287. .cp-list-card-head {
  288. display: flex;
  289. flex-direction: row;
  290. align-items: flex-start;
  291. justify-content: space-between;
  292. margin-bottom: 16rpx;
  293. }
  294. .cp-list-user {
  295. display: flex;
  296. flex-direction: row;
  297. align-items: center;
  298. flex: 1;
  299. min-width: 0;
  300. }
  301. .cp-list-avatar {
  302. width: 72rpx;
  303. height: 72rpx;
  304. border-radius: 50%;
  305. background-color: #eef1f6;
  306. flex-shrink: 0;
  307. }
  308. .cp-list-user-meta {
  309. margin-left: 16rpx;
  310. flex: 1;
  311. min-width: 0;
  312. }
  313. .cp-list-user-name {
  314. display: block;
  315. font-size: 28rpx;
  316. font-weight: 600;
  317. color: #1a1d26;
  318. }
  319. .cp-list-user-time {
  320. display: block;
  321. margin-top: 4rpx;
  322. font-size: 24rpx;
  323. color: #8a8d9e;
  324. }
  325. .fb-list-stars {
  326. display: flex;
  327. flex-direction: row;
  328. flex-shrink: 0;
  329. margin-left: 12rpx;
  330. }
  331. .fb-list-star {
  332. font-size: 28rpx;
  333. color: #d5dae6;
  334. line-height: 1;
  335. }
  336. .fb-list-star--on {
  337. color: #f59e0b;
  338. }
  339. .fb-list-status {
  340. margin-bottom: 12rpx;
  341. }
  342. .fb-list-status-tag {
  343. display: inline-block;
  344. padding: 4rpx 16rpx;
  345. font-size: 22rpx;
  346. color: #4a8cff;
  347. background-color: rgba(74, 140, 255, 0.1);
  348. border-radius: 8rpx;
  349. }
  350. .cp-list-content {
  351. display: block;
  352. font-size: 28rpx;
  353. color: #3d4150;
  354. line-height: 1.55;
  355. word-break: break-word;
  356. }
  357. .cp-list-images {
  358. display: flex;
  359. flex-direction: row;
  360. flex-wrap: wrap;
  361. margin-top: 20rpx;
  362. gap: 12rpx;
  363. }
  364. .cp-list-images--1 .cp-list-img-cell {
  365. width: 100%;
  366. height: 360rpx;
  367. }
  368. .cp-list-images--2 .cp-list-img-cell,
  369. .cp-list-images--3 .cp-list-img-cell {
  370. width: calc(33.33% - 8rpx);
  371. height: 200rpx;
  372. }
  373. .cp-list-img-cell {
  374. border-radius: 16rpx;
  375. overflow: hidden;
  376. background-color: #f0f3f8;
  377. }
  378. .cp-list-img {
  379. width: 100%;
  380. height: 100%;
  381. }
  382. .fb-list-contact {
  383. margin-top: 20rpx;
  384. padding: 16rpx 20rpx;
  385. background-color: #f7f9fc;
  386. border-radius: 12rpx;
  387. }
  388. .fb-list-contact-label {
  389. display: block;
  390. font-size: 22rpx;
  391. color: #8a8d9e;
  392. margin-bottom: 6rpx;
  393. }
  394. .fb-list-contact-text {
  395. font-size: 26rpx;
  396. color: #3d4150;
  397. word-break: break-all;
  398. }
  399. </style>