task-upload-purchase.vue 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. <template>
  2. <view class="page">
  3. <view class="nav-bar">
  4. <view class="nav-left" @tap="goBack">
  5. <text class="nav-icon">‹</text>
  6. <text class="nav-title">外部商品打卡</text>
  7. </view>
  8. </view>
  9. <scroll-view class="content" scroll-y>
  10. <!-- 任务说明 -->
  11. <view class="card">
  12. <view class="card-header">
  13. <text class="card-icon">🛒</text>
  14. <text class="card-title">{{ taskTitle }}</text>
  15. </view>
  16. <view class="card-body">
  17. <text class="task-desc">按以下步骤完成本次外部平台商品购买打卡:</text>
  18. <view class="step-list">
  19. <view class="step-item">
  20. <text class="step-num">1</text>
  21. <text class="step-text">前往外部平台完成商品购买</text>
  22. </view>
  23. <view class="step-item">
  24. <text class="step-num">2</text>
  25. <text class="step-text">收到商品后,拍摄实物照片</text>
  26. </view>
  27. <view class="step-item">
  28. <text class="step-num">3</text>
  29. <text class="step-text">上传照片,提交完成打卡任务</text>
  30. </view>
  31. </view>
  32. </view>
  33. </view>
  34. <!-- 拍照上传 -->
  35. <view class="card">
  36. <view class="card-header">
  37. <text class="card-icon">📸</text>
  38. <text class="card-title">上传商品实物照片</text>
  39. </view>
  40. <view class="card-body">
  41. <view class="upload-area" @tap="choosePhoto">
  42. <image v-if="photoUrl" :src="photoUrl" mode="aspectFill" class="photo-preview" />
  43. <view v-else class="upload-placeholder">
  44. <text class="upload-icon">📷</text>
  45. <text class="upload-text">拍照 / 相册选择商品实物照片</text>
  46. </view>
  47. </view>
  48. <view class="submit-btn" :class="{ submitting: submitting }" @tap="submit">
  49. <text>{{ submitting ? '提交中...' : '✓ 提交完成打卡' }}</text>
  50. </view>
  51. </view>
  52. </view>
  53. <view class="bottom-spacer"></view>
  54. </scroll-view>
  55. </view>
  56. </template>
  57. <script>
  58. import config from '../../config'
  59. export default {
  60. data() {
  61. return {
  62. taskId: null,
  63. memberId: null,
  64. taskTitle: '',
  65. photoUrl: '',
  66. submitting: false
  67. }
  68. },
  69. onLoad(options) {
  70. this.taskId = options && options.taskId ? parseInt(options.taskId) : null
  71. this.taskTitle = options && options.title ? decodeURIComponent(options.title) : '外部平台商品任务'
  72. var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
  73. if (role !== 'child') {
  74. this.memberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || null
  75. } else {
  76. this.memberId = uni.getStorageSync('currentMemberId') || null
  77. }
  78. },
  79. methods: {
  80. goBack() { uni.navigateBack() },
  81. choosePhoto: function() {
  82. var self = this
  83. uni.chooseImage({
  84. count: 1,
  85. sizeType: ['compressed'],
  86. sourceType: ['album', 'camera'],
  87. success: function(res) {
  88. self.photoUrl = res.tempFilePaths[0]
  89. }
  90. })
  91. },
  92. uploadFile: function(filePath) {
  93. var self = this
  94. return new Promise(function(resolve, reject) {
  95. uni.uploadFile({
  96. url: config.API_BASE_URL + '/api/media/upload',
  97. filePath: filePath,
  98. name: 'file',
  99. header: { 'Authorization': 'Bearer ' + uni.getStorageSync('token') },
  100. success: function(res) {
  101. try {
  102. var data = JSON.parse(res.data)
  103. if (data && data.code === 200 && data.data && data.data.url) {
  104. resolve(data.data.url)
  105. } else if (data && data.url) {
  106. resolve(data.url)
  107. } else {
  108. resolve('')
  109. }
  110. } catch (e) {
  111. resolve('')
  112. }
  113. },
  114. fail: function() { reject() }
  115. })
  116. })
  117. },
  118. submit: async function() {
  119. var self = this
  120. if (self.submitting) return
  121. if (!self.photoUrl) { uni.showToast({ title: '请先上传商品实物照片', icon: 'none' }); return }
  122. if (!self.memberId) { uni.showToast({ title: '成员ID无效', icon: 'none' }); return }
  123. self.submitting = true
  124. var photoUrl = ''
  125. try { photoUrl = await self.uploadFile(self.photoUrl) } catch (e) {}
  126. try {
  127. const res = await new Promise(function(resolve, reject) {
  128. uni.request({
  129. url: config.API_BASE_URL + '/api/tasks/' + self.taskId + '/complete',
  130. method: 'POST',
  131. data: { memberId: self.memberId, photoUrl: photoUrl, content: '外部平台商品实物已上传', contentType: 'image' },
  132. header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + uni.getStorageSync('token') },
  133. success: function(r) { r.statusCode && r.statusCode !== 200 ? reject(r) : resolve(r) },
  134. fail: function() { reject() }
  135. })
  136. })
  137. if (res.data && res.data.code === 200) {
  138. uni.showToast({ title: '任务已完成', icon: 'success' })
  139. setTimeout(function() { uni.navigateBack() }, 800)
  140. } else {
  141. uni.showToast({ title: (res.data && res.data.message) || '提交失败', icon: 'none' })
  142. }
  143. } catch (e) {
  144. uni.showToast({ title: '提交失败,请重试', icon: 'none' })
  145. }
  146. self.submitting = false
  147. }
  148. }
  149. }
  150. </script>
  151. <stylescoped lang="scss">
  152. .page { min-height: 100vh; background: $facade-fill-gray-50; }
  153. .nav-bar { height: 88rpx; padding: 24rpx 24rpx 0; display: flex; align-items: flex-end; }
  154. .nav-left { display: flex; align-items: center; gap: 10rpx; }
  155. .nav-icon { font-size: 48rpx; color: $facade-text-strong; line-height: 1; }
  156. .nav-title { font-size: 34rpx; font-weight: 600; color: $facade-text-strong; }
  157. .content { padding-bottom: 40rpx; }
  158. .card { margin: 16rpx 20rpx; background: $app-surface; border-radius: 20rpx; overflow: hidden; box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.05); }
  159. .card-header { display: flex; align-items: center; gap: 10rpx; padding: 20rpx 24rpx 8rpx; border-bottom: 1rpx solid $facade-fill-2; background: linear-gradient(90deg, $uni-primary-pale, $app-surface); }
  160. .card-icon { font-size: 32rpx; }
  161. .card-title { font-size: 30rpx; font-weight: 600; color: $facade-text-strong; }
  162. .card-body { padding: 24rpx; }
  163. .task-desc { font-size: 26rpx; color: #6B7280; line-height: 1.6; display: block; margin-bottom: 20rpx; }
  164. .step-list { display: flex; flex-direction: column; gap: 16rpx; }
  165. .step-item { display: flex; align-items: center; gap: 16rpx; }
  166. .step-num {
  167. width: 44rpx; height: 44rpx; border-radius: 50%;
  168. background: linear-gradient(135deg, $dim-body, $brand-primary);
  169. color: $app-surface; font-size: 24rpx; font-weight: 600;
  170. display: flex; align-items: center; justify-content: center;
  171. flex-shrink: 0;
  172. }
  173. .step-text { font-size: 26rpx; color: #374151; }
  174. .upload-area {
  175. width: 100%; height: 320rpx;
  176. border: 2rpx dashed #D1D5DB; border-radius: 16rpx;
  177. display: flex; align-items: center; justify-content: center;
  178. background: $facade-fill-gray-50; overflow: hidden;
  179. margin-bottom: 24rpx;
  180. }
  181. .photo-preview { width: 100%; height: 100%; }
  182. .upload-placeholder { display: flex; flex-direction: column; align-items: center; gap: 12rpx; }
  183. .upload-icon { font-size: 72rpx; }
  184. .upload-text { font-size: 26rpx; color: #9CA3AF; }
  185. .submit-btn {
  186. background: linear-gradient(135deg, $dim-body, $brand-primary);
  187. color: $app-surface; text-align: center; padding: 22rpx 0;
  188. border-radius: 14rpx; font-size: 30rpx; font-weight: 600;
  189. box-shadow: 0 6rpx 20rpx rgba(249,115,22,0.3);
  190. }
  191. .submit-btn:active { transform: scale(0.98); }
  192. .submitting { opacity: 0.6; }
  193. .bottom-spacer { height: 40rpx; }
  194. </style>