tasks.vue 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. <template>
  2. <PlayfulCard elevation="1">
  3. <view class="container">
  4. <!-- 任务列表 -->
  5. <view class="task-list" v-if="tasks.length > 0">
  6. <view
  7. v-for="task in tasks"
  8. :key="task.id"
  9. class="task-item"
  10. :class="{ completed: task.status === 'completed' }"
  11. >
  12. <view class="task-left">
  13. <view class="task-title">{{ task.title }}</view>
  14. <view class="task-meta">
  15. <text class="points">+{{ task.points }}分</text>
  16. <text class="deadline">截止 {{ formatTime(task.deadline) }}</text>
  17. </view>
  18. </view>
  19. <view class="task-right">
  20. <button
  21. v-if="task.status === 'pending'"
  22. class="btn-complete"
  23. @click="completeTask(task.id)"
  24. >
  25. 完成
  26. </button>
  27. <view v-else class="completed-badge">已完成</view>
  28. </view>
  29. </view>
  30. </view>
  31. <view class="empty" v-else>
  32. <text>暂无今日任务</text>
  33. </view>
  34. <!-- 家长模式:添加任务按钮 -->
  35. <button
  36. v-if="isParent"
  37. class="btn-primary add-btn"
  38. @click="showAddModal = true"
  39. >
  40. + 添加任务
  41. </button>
  42. <!-- 添加任务弹窗 -->
  43. <view class="modal-mask" v-if="showAddModal" @click="showAddModal = false">
  44. <view class="modal" @click.stop>
  45. <view class="modal-title">添加任务</view>
  46. <view class="form-item">
  47. <input v-model="newTask.title" placeholder="任务名称" />
  48. </view>
  49. <view class="form-item">
  50. <input v-model="newTask.points" type="number" placeholder="积分(1-10)" />
  51. </view>
  52. <view class="form-item">
  53. <picker mode="time" @change="onTimeChange">
  54. <view class="picker">
  55. {{ newTask.deadlineTime || '选择截止时间' }}
  56. </view>
  57. </picker>
  58. </view>
  59. <view class="form-item">
  60. <picker mode="selector" :range="categories" @change="onCategoryChange">
  61. <view class="picker">
  62. {{ newTask.category || '选择分类' }}
  63. </view>
  64. </picker>
  65. </view>
  66. <view class="modal-btns">
  67. <button @click="showAddModal = false">取消</button>
  68. <button class="btn-primary" @click="createTask">确定</button>
  69. </view>
  70. </view>
  71. </view>
  72. </view>
  73. </PlayfulCard>
  74. </template>
  75. <script>
  76. import { getTodayTasks, completeTask, createTask, getChildren } from '../../utils/api.js'
  77. import PlayfulCard from '@/components/PlayfulCard.vue'
  78. export default {
  79. components: { PlayfulCard },
  80. data() {
  81. return {
  82. tasks: [],
  83. isParent: false,
  84. currentChildId: '',
  85. showAddModal: false,
  86. newTask: {
  87. title: '',
  88. points: 2,
  89. deadlineTime: '',
  90. category: ''
  91. },
  92. categories: ['学习类', '生活类', '运动类', '其他']
  93. }
  94. },
  95. onShow() {
  96. this.loadTasks()
  97. this.isParent = this.$store.state.role === 'parent'
  98. },
  99. methods: {
  100. async loadTasks() {
  101. try {
  102. const childrenRes = await getChildren()
  103. const children = childrenRes.data
  104. if (children && children.length > 0) {
  105. this.currentChildId = children[0].id
  106. const res = await getTodayTasks(this.currentChildId)
  107. this.tasks = res.data || []
  108. }
  109. } catch (e) {
  110. console.error('加载任务失败', e)
  111. }
  112. },
  113. async completeTask(taskId) {
  114. try {
  115. const res = await completeTask(taskId, this.currentChildId, '')
  116. uni.showToast({
  117. title: `获得 ${res.data.pointsEarned} 积分`,
  118. icon: 'success'
  119. })
  120. this.loadTasks()
  121. } catch (e) {
  122. console.error('完成任务失败', e)
  123. }
  124. },
  125. onTimeChange(e) {
  126. const now = new Date()
  127. const deadline = new Date(now.getFullYear(), now.getMonth(), now.getDate(), e.detail.value)
  128. this.newTask.deadlineTime = e.detail.value
  129. },
  130. onCategoryChange(e) {
  131. this.newTask.category = this.categories[e.detail.value]
  132. },
  133. async createTask() {
  134. if (!this.newTask.title) {
  135. uni.showToast({ title: '请输入任务名称', icon: 'none' })
  136. return
  137. }
  138. const now = new Date()
  139. const deadline = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1)
  140. const [hours, minutes] = (this.newTask.deadlineTime || '20:00').split(':')
  141. deadline.setHours(parseInt(hours), parseInt(minutes), 0, 0)
  142. try {
  143. await createTask({
  144. title: this.newTask.title,
  145. points: parseInt(this.newTask.points),
  146. deadline: deadline.toISOString(),
  147. category: this.newTask.category,
  148. childId: this.currentChildId
  149. })
  150. uni.showToast({ title: '创建成功', icon: 'success' })
  151. this.showAddModal = false
  152. this.loadTasks()
  153. } catch (e) {
  154. console.error('创建任务失败', e)
  155. }
  156. },
  157. formatTime(dateStr) {
  158. if (!dateStr) return ''
  159. const date = new Date(dateStr)
  160. return `${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`
  161. }
  162. }
  163. }
  164. </script>
  165. <style scoped>
  166. .container {
  167. padding: 30rpx;
  168. padding-bottom: 120rpx;
  169. }
  170. .task-item {
  171. background: #fff;
  172. border-radius: 16rpx;
  173. padding: 30rpx;
  174. margin-bottom: 20rpx;
  175. display: flex;
  176. justify-content: space-between;
  177. align-items: center;
  178. }
  179. .task-item.completed {
  180. opacity: 0.6;
  181. }
  182. .task-title {
  183. font-size: 30rpx;
  184. color: #333;
  185. margin-bottom: 10rpx;
  186. }
  187. .task-meta {
  188. display: flex;
  189. gap: 20rpx;
  190. }
  191. .points {
  192. color: #FF6B6B;
  193. font-size: 24rpx;
  194. }
  195. .deadline {
  196. color: #999;
  197. font-size: 24rpx;
  198. }
  199. .btn-complete {
  200. background: #FF6B6B;
  201. color: #fff;
  202. font-size: 26rpx;
  203. padding: 10rpx 30rpx;
  204. border-radius: 30rpx;
  205. }
  206. .completed-badge {
  207. color: #52C41A;
  208. font-size: 26rpx;
  209. }
  210. .add-btn {
  211. position: fixed;
  212. bottom: 30rpx;
  213. left: 30rpx;
  214. right: 30rpx;
  215. }
  216. .empty {
  217. text-align: center;
  218. color: #999;
  219. padding: 100rpx;
  220. }
  221. .modal-mask {
  222. position: fixed;
  223. top: 0;
  224. left: 0;
  225. right: 0;
  226. bottom: 0;
  227. background: rgba(0, 0, 0, 0.5);
  228. display: flex;
  229. align-items: center;
  230. justify-content: center;
  231. }
  232. .modal {
  233. background: #fff;
  234. border-radius: 20rpx;
  235. padding: 40rpx;
  236. width: 80%;
  237. }
  238. .modal-title {
  239. font-size: 32rpx;
  240. font-weight: bold;
  241. margin-bottom: 30rpx;
  242. text-align: center;
  243. }
  244. .form-item {
  245. margin-bottom: 20rpx;
  246. }
  247. .form-item input, .picker {
  248. border: 1rpx solid #ddd;
  249. border-radius: 10rpx;
  250. padding: 20rpx;
  251. font-size: 28rpx;
  252. }
  253. .modal-btns {
  254. display: flex;
  255. gap: 20rpx;
  256. margin-top: 30rpx;
  257. }
  258. .modal-btns button {
  259. flex: 1;
  260. }
  261. </style>