report.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. <template>
  2. <view class="report-page">
  3. <!-- 自定义导航栏 -->
  4. <view class="custom-navbar">
  5. <view class="navbar-left" @click="goBack">
  6. <text class="back-icon">←</text>
  7. </view>
  8. <view class="navbar-title">
  9. <text class="title-text">举报动态</text>
  10. </view>
  11. <view class="navbar-right"></view>
  12. </view>
  13. <!-- 内容区域 -->
  14. <scroll-view class="content-scroll" scroll-y>
  15. <view class="form-container">
  16. <!-- 温馨提示 -->
  17. <view class="tip-card">
  18. <text class="tip-icon">💡</text>
  19. <text class="tip-text">我们会认真处理每一条举报,请如实填写相关信息</text>
  20. </view>
  21. <!-- 举报原因 -->
  22. <view class="form-section">
  23. <view class="section-label required">举报原因</view>
  24. <view class="reason-list">
  25. <view
  26. v-for="item in reasonOptions"
  27. :key="item.value"
  28. :class="['reason-item', { 'active': reportData.reportType === item.value }]"
  29. @click="selectReason(item.value)"
  30. >
  31. <text class="reason-icon">{{ item.icon }}</text>
  32. <text class="reason-text">{{ item.label }}</text>
  33. <text v-if="reportData.reportType === item.value" class="check-icon">✓</text>
  34. </view>
  35. </view>
  36. <!-- 其他原因输入框 -->
  37. <view v-if="reportData.reportType === 'other'" class="other-input">
  38. <textarea
  39. v-model="otherReason"
  40. placeholder="请输入具体原因"
  41. class="other-textarea"
  42. maxlength="50"
  43. />
  44. <text class="char-count">{{ otherReason.length }}/50</text>
  45. </view>
  46. </view>
  47. <!-- 截图上传 -->
  48. <view class="form-section">
  49. <view class="section-label">
  50. <text>举报截图</text>
  51. <text class="label-hint">(选填,最多3张)</text>
  52. </view>
  53. <view class="upload-area">
  54. <view class="upload-tip">
  55. <text class="upload-tip-icon">📷</text>
  56. <text class="upload-tip-text">点击上传截图,帮助我们更快处理</text>
  57. </view>
  58. <view class="image-list">
  59. <view
  60. v-for="(img, index) in reportData.screenshots"
  61. :key="index"
  62. class="image-item"
  63. >
  64. <image :src="img" mode="aspectFill" class="preview-image" />
  65. <view class="remove-btn" @click="removeImage(index)">
  66. <text class="remove-icon">×</text>
  67. </view>
  68. </view>
  69. <view
  70. v-if="reportData.screenshots.length < 3"
  71. class="add-image"
  72. @click="chooseImage"
  73. >
  74. <text class="add-icon">+</text>
  75. <text class="add-text">添加图片</text>
  76. </view>
  77. </view>
  78. </view>
  79. </view>
  80. <!-- 详细描述 -->
  81. <view class="form-section">
  82. <view class="section-label">
  83. <text>详细描述</text>
  84. <text class="label-hint">(选填,20-500字)</text>
  85. </view>
  86. <textarea
  87. v-model="reportData.description"
  88. placeholder="请详细描述举报事由、涉事内容位置等信息,以便我们更好地处理..."
  89. class="description-textarea"
  90. maxlength="500"
  91. />
  92. <view class="textarea-footer">
  93. <text class="char-count">{{ reportData.description.length }}/500</text>
  94. </view>
  95. </view>
  96. <!-- 联系方式 -->
  97. <view class="form-section">
  98. <view class="section-label">
  99. <text>联系方式</text>
  100. <text class="label-hint">(选填,便于反馈处理结果)</text>
  101. </view>
  102. <input
  103. v-model="reportData.contact"
  104. placeholder="请输入手机号或微信号"
  105. class="contact-input"
  106. maxlength="50"
  107. />
  108. </view>
  109. </view>
  110. </scroll-view>
  111. <!-- 底部提交按钮 -->
  112. <view class="bottom-bar">
  113. <button
  114. class="submit-btn"
  115. :disabled="!canSubmit || submitting"
  116. :class="{ 'disabled': !canSubmit || submitting }"
  117. @click="submitReport"
  118. >
  119. <text v-if="!submitting">{{ canSubmit ? '提交举报' : '请选择举报原因' }}</text>
  120. <text v-else>提交中...</text>
  121. </button>
  122. </view>
  123. </view>
  124. </template>
  125. <script>
  126. import api from '@/utils/api.js'
  127. export default {
  128. data() {
  129. return {
  130. dynamicId: null, // 被举报的动态ID
  131. reportData: {
  132. reportType: '', // 举报类型
  133. description: '', // 详细描述
  134. screenshots: [], // 截图列表
  135. contact: '' // 联系方式
  136. },
  137. otherReason: '', // 其他原因文本
  138. submitting: false, // 是否正在提交
  139. reasonOptions: [
  140. { value: 'spam', label: '垃圾广告', icon: '🚫' },
  141. { value: 'porn', label: '色情低俗', icon: '🔞' },
  142. { value: 'violence', label: '暴力违法', icon: '⚠️' },
  143. { value: 'attack', label: '人身攻击', icon: '💢' },
  144. { value: 'fake', label: '虚假信息', icon: '❌' },
  145. { value: 'plagiarism', label: '抄袭侵权', icon: '©️' },
  146. { value: 'other', label: '其他', icon: '📝' }
  147. ]
  148. }
  149. },
  150. computed: {
  151. // 是否可以提交
  152. canSubmit() {
  153. if (!this.reportData.reportType) {
  154. return false
  155. }
  156. // 如果选择了"其他",必须填写具体原因
  157. if (this.reportData.reportType === 'other' && !this.otherReason.trim()) {
  158. return false
  159. }
  160. return true
  161. }
  162. },
  163. onLoad(options) {
  164. if (options.id) {
  165. this.dynamicId = options.id
  166. }
  167. },
  168. methods: {
  169. // 选择举报原因
  170. selectReason(value) {
  171. this.reportData.reportType = value
  172. // 如果不是"其他",清空其他原因文本
  173. if (value !== 'other') {
  174. this.otherReason = ''
  175. }
  176. },
  177. // 选择图片
  178. chooseImage() {
  179. const maxCount = 3 - this.reportData.screenshots.length
  180. if (maxCount <= 0) {
  181. uni.showToast({
  182. title: '最多只能上传3张图片',
  183. icon: 'none'
  184. })
  185. return
  186. }
  187. uni.chooseImage({
  188. count: maxCount,
  189. sizeType: ['compressed'],
  190. sourceType: ['album', 'camera'],
  191. success: async (res) => {
  192. const filePaths = res.tempFilePaths || []
  193. if (filePaths.length === 0) return
  194. // 显示上传进度
  195. uni.showLoading({ title: '上传中...' })
  196. try {
  197. // 逐个上传图片
  198. for (let filePath of filePaths) {
  199. try {
  200. const url = await api.dynamic.uploadSingle(filePath)
  201. this.reportData.screenshots.push(url)
  202. } catch (error) {
  203. console.error('上传图片失败:', error)
  204. uni.showToast({
  205. title: '部分图片上传失败',
  206. icon: 'none'
  207. })
  208. }
  209. }
  210. } finally {
  211. uni.hideLoading()
  212. }
  213. },
  214. fail: () => {
  215. uni.showToast({
  216. title: '选择图片失败',
  217. icon: 'none'
  218. })
  219. }
  220. })
  221. },
  222. // 移除图片
  223. removeImage(index) {
  224. this.reportData.screenshots.splice(index, 1)
  225. },
  226. // 提交举报
  227. async submitReport() {
  228. if (!this.canSubmit || this.submitting) {
  229. return
  230. }
  231. // 二次确认
  232. const confirm = await new Promise((resolve) => {
  233. uni.showModal({
  234. title: '确认提交',
  235. content: '确定要举报该动态吗?',
  236. success: (res) => {
  237. resolve(res.confirm)
  238. },
  239. fail: () => {
  240. resolve(false)
  241. }
  242. })
  243. })
  244. if (!confirm) {
  245. return
  246. }
  247. this.submitting = true
  248. try {
  249. // 构造提交数据
  250. const submitData = {
  251. dynamicId: parseInt(this.dynamicId),
  252. reporterId: 1, // TODO: 从登录信息获取真实用户ID
  253. reportType: this.reportData.reportType,
  254. description: this.reportData.description.trim(),
  255. screenshots: this.reportData.screenshots,
  256. contact: this.reportData.contact.trim()
  257. }
  258. // 如果选择了"其他",将具体原因追加到描述中
  259. if (this.reportData.reportType === 'other' && this.otherReason.trim()) {
  260. submitData.description = `[其他原因: ${this.otherReason.trim()}] ${submitData.description}`
  261. }
  262. // 提交举报
  263. await api.dynamic.submitReport(submitData)
  264. // 提交成功
  265. uni.showToast({
  266. title: '举报已提交',
  267. icon: 'success',
  268. duration: 2000
  269. })
  270. // 延迟返回
  271. setTimeout(() => {
  272. uni.navigateBack()
  273. }, 2000)
  274. } catch (error) {
  275. console.error('提交举报失败:', error)
  276. uni.showToast({
  277. title: '提交失败,请重试',
  278. icon: 'none'
  279. })
  280. } finally {
  281. this.submitting = false
  282. }
  283. },
  284. // 返回
  285. goBack() {
  286. uni.navigateBack()
  287. }
  288. }
  289. }
  290. </script>
  291. <style lang="scss" scoped>
  292. .report-page {
  293. min-height: 100vh;
  294. background: #FFF0F5;
  295. // 自定义导航栏
  296. .custom-navbar {
  297. position: fixed;
  298. top: 0;
  299. left: 0;
  300. right: 0;
  301. height: 88rpx;
  302. background: #FFFFFF;
  303. display: flex;
  304. align-items: center;
  305. padding: 0 30rpx;
  306. z-index: 1000;
  307. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  308. .navbar-left,
  309. .navbar-right {
  310. width: 80rpx;
  311. }
  312. .navbar-left {
  313. .back-icon {
  314. font-size: 40rpx;
  315. color: #E91E63;
  316. }
  317. }
  318. .navbar-title {
  319. flex: 1;
  320. text-align: center;
  321. .title-text {
  322. font-size: 32rpx;
  323. font-weight: 600;
  324. color: #333333;
  325. }
  326. }
  327. }
  328. // 内容滚动区域
  329. .content-scroll {
  330. margin-top: 88rpx;
  331. height: calc(100vh - 88rpx - 140rpx);
  332. .form-container {
  333. padding: 24rpx;
  334. }
  335. // 温馨提示卡片
  336. .tip-card {
  337. background: linear-gradient(135deg, #FFF5F7 0%, #FFE8EC 100%);
  338. border-radius: 16rpx;
  339. padding: 24rpx;
  340. margin-bottom: 24rpx;
  341. display: flex;
  342. align-items: center;
  343. border: 1rpx solid rgba(233, 30, 99, 0.1);
  344. .tip-icon {
  345. font-size: 36rpx;
  346. margin-right: 16rpx;
  347. }
  348. .tip-text {
  349. flex: 1;
  350. font-size: 26rpx;
  351. color: #C2185B;
  352. line-height: 1.6;
  353. }
  354. }
  355. // 表单区块
  356. .form-section {
  357. background: #FFFFFF;
  358. border-radius: 16rpx;
  359. padding: 28rpx;
  360. margin-bottom: 24rpx;
  361. box-shadow: 0 4rpx 12rpx rgba(233, 30, 99, 0.06);
  362. .section-label {
  363. font-size: 30rpx;
  364. font-weight: 600;
  365. color: #333333;
  366. margin-bottom: 20rpx;
  367. display: flex;
  368. align-items: center;
  369. &.required::before {
  370. content: '*';
  371. color: #FF4444;
  372. margin-right: 6rpx;
  373. font-size: 32rpx;
  374. }
  375. .label-hint {
  376. font-size: 24rpx;
  377. color: #999999;
  378. font-weight: normal;
  379. margin-left: 8rpx;
  380. }
  381. }
  382. // 举报原因列表
  383. .reason-list {
  384. display: grid;
  385. grid-template-columns: repeat(2, 1fr);
  386. gap: 16rpx;
  387. }
  388. .reason-item {
  389. background: #F8F8F8;
  390. border: 2rpx solid transparent;
  391. border-radius: 12rpx;
  392. padding: 24rpx 16rpx;
  393. display: flex;
  394. flex-direction: column;
  395. align-items: center;
  396. position: relative;
  397. transition: all 0.3s ease;
  398. &.active {
  399. background: #FFF5F7;
  400. border-color: #E91E63;
  401. .reason-icon {
  402. transform: scale(1.2);
  403. }
  404. }
  405. .reason-icon {
  406. font-size: 40rpx;
  407. margin-bottom: 12rpx;
  408. transition: transform 0.3s ease;
  409. }
  410. .reason-text {
  411. font-size: 26rpx;
  412. color: #333333;
  413. }
  414. .check-icon {
  415. position: absolute;
  416. top: 8rpx;
  417. right: 8rpx;
  418. font-size: 28rpx;
  419. color: #E91E63;
  420. font-weight: bold;
  421. }
  422. }
  423. // 其他原因输入框
  424. .other-input {
  425. margin-top: 16rpx;
  426. position: relative;
  427. .other-textarea {
  428. width: 100%;
  429. min-height: 120rpx;
  430. background: #F8F8F8;
  431. border-radius: 12rpx;
  432. padding: 16rpx;
  433. font-size: 28rpx;
  434. line-height: 1.6;
  435. }
  436. .char-count {
  437. position: absolute;
  438. bottom: 12rpx;
  439. right: 12rpx;
  440. font-size: 22rpx;
  441. color: #999999;
  442. }
  443. }
  444. // 上传区域
  445. .upload-area {
  446. .upload-tip {
  447. background: #FFF9E6;
  448. border-radius: 12rpx;
  449. padding: 16rpx;
  450. margin-bottom: 20rpx;
  451. display: flex;
  452. align-items: center;
  453. border: 1rpx dashed #FFD700;
  454. .upload-tip-icon {
  455. font-size: 32rpx;
  456. margin-right: 12rpx;
  457. }
  458. .upload-tip-text {
  459. flex: 1;
  460. font-size: 24rpx;
  461. color: #FF8C00;
  462. line-height: 1.5;
  463. }
  464. }
  465. .image-list {
  466. display: flex;
  467. flex-wrap: wrap;
  468. gap: 16rpx;
  469. }
  470. .image-item {
  471. position: relative;
  472. width: 160rpx;
  473. height: 160rpx;
  474. border-radius: 12rpx;
  475. overflow: hidden;
  476. .preview-image {
  477. width: 100%;
  478. height: 100%;
  479. }
  480. .remove-btn {
  481. position: absolute;
  482. top: 8rpx;
  483. right: 8rpx;
  484. width: 40rpx;
  485. height: 40rpx;
  486. background: rgba(0, 0, 0, 0.6);
  487. border-radius: 50%;
  488. display: flex;
  489. align-items: center;
  490. justify-content: center;
  491. .remove-icon {
  492. font-size: 32rpx;
  493. color: #FFFFFF;
  494. font-weight: bold;
  495. line-height: 1;
  496. }
  497. }
  498. }
  499. .add-image {
  500. width: 160rpx;
  501. height: 160rpx;
  502. background: #F8F8F8;
  503. border: 2rpx dashed #CCCCCC;
  504. border-radius: 12rpx;
  505. display: flex;
  506. flex-direction: column;
  507. align-items: center;
  508. justify-content: center;
  509. .add-icon {
  510. font-size: 48rpx;
  511. color: #CCCCCC;
  512. margin-bottom: 8rpx;
  513. }
  514. .add-text {
  515. font-size: 22rpx;
  516. color: #999999;
  517. }
  518. }
  519. }
  520. // 详细描述文本框
  521. .description-textarea {
  522. width: 100%;
  523. min-height: 200rpx;
  524. background: #F8F8F8;
  525. border-radius: 12rpx;
  526. padding: 16rpx;
  527. font-size: 28rpx;
  528. line-height: 1.6;
  529. }
  530. .textarea-footer {
  531. display: flex;
  532. justify-content: flex-end;
  533. margin-top: 12rpx;
  534. .char-count {
  535. font-size: 24rpx;
  536. color: #999999;
  537. }
  538. }
  539. // 联系方式输入框
  540. .contact-input {
  541. width: 100%;
  542. height: 88rpx;
  543. background: #F8F8F8;
  544. border-radius: 12rpx;
  545. padding: 0 20rpx;
  546. font-size: 28rpx;
  547. }
  548. }
  549. }
  550. // 底部按钮
  551. .bottom-bar {
  552. position: fixed;
  553. bottom: 0;
  554. left: 0;
  555. right: 0;
  556. background: #FFFFFF;
  557. padding: 20rpx 30rpx;
  558. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
  559. padding-bottom: constant(safe-area-inset-bottom);
  560. padding-bottom: env(safe-area-inset-bottom);
  561. .submit-btn {
  562. width: 100%;
  563. height: 88rpx;
  564. background: linear-gradient(135deg, #E91E63 0%, #FF6B9D 100%);
  565. color: #FFFFFF;
  566. border: none;
  567. border-radius: 44rpx;
  568. font-size: 32rpx;
  569. font-weight: 600;
  570. display: flex;
  571. align-items: center;
  572. justify-content: center;
  573. box-shadow: 0 8rpx 24rpx rgba(233, 30, 99, 0.3);
  574. transition: all 0.3s ease;
  575. &.disabled {
  576. background: #CCCCCC;
  577. box-shadow: none;
  578. opacity: 0.6;
  579. }
  580. &:not(.disabled):active {
  581. transform: scale(0.98);
  582. }
  583. }
  584. }
  585. }
  586. </style>