write-review.vue 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. <template>
  2. <view class="container">
  3. <!-- 顶部导航栏 -->
  4. <view class="header">
  5. <text class="header-title">写书评</text>
  6. </view>
  7. <!-- 评分选择 -->
  8. <view class="rating-section">
  9. <view
  10. class="rating-btn"
  11. v-for="(rating, index) in ratingOptions"
  12. :key="index"
  13. :class="{ active: selectedRating === rating.value }"
  14. @click="selectRating(rating.value)"
  15. >
  16. <text class="rating-text">{{ rating.label }}</text>
  17. </view>
  18. </view>
  19. <!-- 书评输入区域 -->
  20. <view class="review-input-section">
  21. <textarea
  22. class="review-input"
  23. v-model="reviewContent"
  24. placeholder="写书评..."
  25. maxlength="500"
  26. :auto-focus="true"
  27. @input="handleInput"
  28. ></textarea>
  29. <view class="char-count">
  30. <text class="char-count-text">{{ reviewContent.length }}/500</text>
  31. </view>
  32. </view>
  33. <!-- 底部操作栏 -->
  34. <view class="action-bar">
  35. <view class="cancel-btn" @click="handleCancel">
  36. <text class="action-text">取消</text>
  37. </view>
  38. <view class="publish-btn" :class="{ disabled: !canPublish }" @click="handlePublish">
  39. <text class="action-text">发表</text>
  40. </view>
  41. </view>
  42. </view>
  43. </template>
  44. <script>
  45. import { addComment } from '../../utils/api.js'
  46. export default {
  47. data() {
  48. return {
  49. bookInfo: {
  50. id: null,
  51. title: ''
  52. },
  53. selectedRating: 'recommend', // recommend, general, bad
  54. ratingOptions: [
  55. { label: '推荐', value: 'recommend' },
  56. { label: '一般', value: 'general' },
  57. { label: '不好', value: 'bad' }
  58. ],
  59. reviewContent: '',
  60. userInfo: null,
  61. isSubmitting: false
  62. }
  63. },
  64. computed: {
  65. canPublish() {
  66. return this.reviewContent.trim().length > 0 && !this.isSubmitting
  67. }
  68. },
  69. onLoad(options) {
  70. // 获取用户信息
  71. try {
  72. const userInfo = uni.getStorageSync('userInfo')
  73. if (userInfo && userInfo.id) {
  74. this.userInfo = userInfo
  75. }
  76. } catch (e) {
  77. console.error('获取用户信息失败', e)
  78. }
  79. if (options.bookId) {
  80. this.bookInfo.id = parseInt(options.bookId)
  81. }
  82. if (options.title) {
  83. this.bookInfo.title = decodeURIComponent(options.title)
  84. }
  85. },
  86. methods: {
  87. selectRating(value) {
  88. this.selectedRating = value
  89. },
  90. handleInput(e) {
  91. this.reviewContent = e.detail.value
  92. },
  93. handleCancel() {
  94. uni.showModal({
  95. title: '提示',
  96. content: '确定要取消吗?未保存的内容将丢失。',
  97. success: (res) => {
  98. if (res.confirm) {
  99. uni.navigateBack()
  100. }
  101. }
  102. })
  103. },
  104. async handlePublish() {
  105. if (!this.canPublish) {
  106. uni.showToast({
  107. title: '请输入书评内容',
  108. icon: 'none'
  109. })
  110. return
  111. }
  112. // 检查用户是否登录
  113. if (!this.userInfo || !this.userInfo.id) {
  114. uni.showModal({
  115. title: '提示',
  116. content: '请先登录',
  117. showCancel: true,
  118. cancelText: '取消',
  119. confirmText: '去登录',
  120. success: (res) => {
  121. if (res.confirm) {
  122. uni.navigateTo({
  123. url: '/pages/login/login'
  124. })
  125. }
  126. }
  127. })
  128. return
  129. }
  130. // 检查书籍ID
  131. if (!this.bookInfo.id) {
  132. uni.showToast({
  133. title: '书籍信息不完整',
  134. icon: 'none'
  135. })
  136. return
  137. }
  138. try {
  139. this.isSubmitting = true
  140. uni.showLoading({
  141. title: '发表中...',
  142. mask: true
  143. })
  144. const res = await addComment(
  145. this.userInfo.id,
  146. this.bookInfo.id,
  147. this.reviewContent.trim()
  148. )
  149. uni.hideLoading()
  150. this.isSubmitting = false
  151. if (res && res.code === 200) {
  152. uni.showToast({
  153. title: '发表成功',
  154. icon: 'success',
  155. duration: 2000
  156. })
  157. setTimeout(() => {
  158. uni.navigateBack()
  159. }, 2000)
  160. } else {
  161. uni.showToast({
  162. title: res.message || '发表失败',
  163. icon: 'none'
  164. })
  165. }
  166. } catch (e) {
  167. uni.hideLoading()
  168. this.isSubmitting = false
  169. console.error('发表评论失败', e)
  170. uni.showToast({
  171. title: e.message || '发表失败,请重试',
  172. icon: 'none'
  173. })
  174. }
  175. }
  176. }
  177. }
  178. </script>
  179. <style scoped>
  180. .container {
  181. width: 100%;
  182. height: 100vh;
  183. background-color: #FFFFFF;
  184. display: flex;
  185. flex-direction: column;
  186. padding-top: 30px;
  187. box-sizing: border-box;
  188. }
  189. .header {
  190. display: flex;
  191. align-items: center;
  192. justify-content: center;
  193. padding: 20rpx 30rpx;
  194. padding-top: calc(20rpx + env(safe-area-inset-top));
  195. background-color: #F5F5F5;
  196. border-bottom: 1rpx solid #E0E0E0;
  197. position: relative;
  198. }
  199. .header-title {
  200. font-size: 36rpx;
  201. font-weight: bold;
  202. color: #333333;
  203. }
  204. .rating-section {
  205. display: flex;
  206. justify-content: center;
  207. align-items: center;
  208. padding: 30rpx;
  209. gap: 20rpx;
  210. background-color: #F5F5F5;
  211. border-bottom: 1rpx solid #E0E0E0;
  212. }
  213. .rating-btn {
  214. flex: 1;
  215. height: 70rpx;
  216. display: flex;
  217. align-items: center;
  218. justify-content: center;
  219. background-color: #E0E0E0;
  220. border-radius: 8rpx;
  221. border: 2rpx solid #E0E0E0;
  222. transition: all 0.3s;
  223. }
  224. .rating-btn.active {
  225. background-color: #4CAF50;
  226. border-color: #4CAF50;
  227. }
  228. .rating-btn.active .rating-text {
  229. color: #FFFFFF;
  230. }
  231. .rating-text {
  232. font-size: 28rpx;
  233. color: #666666;
  234. font-weight: 500;
  235. }
  236. .review-input-section {
  237. flex: 1;
  238. display: flex;
  239. flex-direction: column;
  240. padding: 30rpx;
  241. background-color: #FFFFFF;
  242. }
  243. .review-input {
  244. flex: 1;
  245. width: 100%;
  246. min-height: 400rpx;
  247. padding: 20rpx;
  248. background-color: #FFFFFF;
  249. border: 1rpx solid #E0E0E0;
  250. border-radius: 8rpx;
  251. font-size: 30rpx;
  252. color: #333333;
  253. line-height: 1.8;
  254. box-sizing: border-box;
  255. }
  256. .review-input::placeholder {
  257. color: #CCCCCC;
  258. }
  259. .char-count {
  260. display: flex;
  261. justify-content: flex-end;
  262. margin-top: 20rpx;
  263. }
  264. .char-count-text {
  265. font-size: 24rpx;
  266. color: #999999;
  267. }
  268. .action-bar {
  269. display: flex;
  270. align-items: center;
  271. justify-content: space-between;
  272. padding: 20rpx 30rpx;
  273. background-color: #F5F5F5;
  274. border-top: 1rpx solid #E0E0E0;
  275. padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  276. }
  277. .cancel-btn,
  278. .publish-btn {
  279. flex: 1;
  280. height: 80rpx;
  281. display: flex;
  282. align-items: center;
  283. justify-content: center;
  284. border-radius: 8rpx;
  285. transition: all 0.3s;
  286. }
  287. .cancel-btn {
  288. margin-right: 20rpx;
  289. background-color: #FFFFFF;
  290. border: 1rpx solid #E0E0E0;
  291. }
  292. .publish-btn {
  293. background-color: #4FC3F7;
  294. border: none;
  295. }
  296. .publish-btn.disabled {
  297. background-color: #CCCCCC;
  298. opacity: 0.6;
  299. }
  300. .action-text {
  301. font-size: 32rpx;
  302. color: #333333;
  303. font-weight: 500;
  304. }
  305. .publish-btn .action-text {
  306. color: #FFFFFF;
  307. }
  308. .publish-btn.disabled .action-text {
  309. color: #FFFFFF;
  310. }
  311. </style>