create-task.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. <template>
  2. <view class="container">
  3. <view class="form-container">
  4. <view class="form-item">
  5. <text class="label">任务名称</text>
  6. <input class="input" v-model="form.title" placeholder="请输入任务名称" />
  7. </view>
  8. <view class="form-item">
  9. <text class="label">任务描述</text>
  10. <textarea class="textarea" v-model="form.description" placeholder="请输入任务描述" />
  11. </view>
  12. <view class="form-item">
  13. <text class="label">选择孩子(可多选)</text>
  14. <checkbox-group @change="onChildrenChange">
  15. <label class="child-checkbox" v-for="child in children" :key="child.id">
  16. <checkbox :value="String(child.id)" :checked="selectedChildIds.includes(String(child.id))" />
  17. <text class="child-name">{{ child.nickname }}</text>
  18. </label>
  19. </checkbox-group>
  20. </view>
  21. <view class="form-item">
  22. <text class="label">积分</text>
  23. <input class="input" type="number" v-model="form.points" placeholder="请输入积分 (1-10)" />
  24. </view>
  25. <view class="form-item">
  26. <text class="label">分类</text>
  27. <picker :range="categories" @change="onCategoryChange">
  28. <view class="picker">
  29. {{ form.category || '请选择分类' }}
  30. </view>
  31. </picker>
  32. </view>
  33. <!-- 小游戏选择(当选择"小游戏类"时显示) -->
  34. <view class="form-item" v-if="form.category === '小游戏类'">
  35. <text class="label">选择小游戏</text>
  36. <picker :range="miniGames" range-key="gameName" @change="onMinigameChange">
  37. <view class="picker">
  38. {{ selectedMinigame ? selectedMinigame.gameName : '请选择小游戏' }}
  39. </view>
  40. </picker>
  41. </view>
  42. <!-- TASK-003: 截止时间设置 -->
  43. <view class="form-item">
  44. <text class="label">截止时间</text>
  45. <picker mode="multiSelector" :range="dateTimeRange" :value="dateTimeIndex" @change="onDateTimeChange" @columnchange="onColumnChange">
  46. <view class="picker">
  47. {{ deadlineText || '请选择截止时间' }}
  48. </view>
  49. </picker>
  50. </view>
  51. <!-- TASK-004: 重复设置 -->
  52. <view class="form-item">
  53. <text class="label">重复设置</text>
  54. <picker :range="repeatTypes" range-key="label" @change="onRepeatChange">
  55. <view class="picker">
  56. {{ repeatTypeLabel || '不重复' }}
  57. </view>
  58. </picker>
  59. </view>
  60. <view class="form-item">
  61. <text class="label">需要审核</text>
  62. <switch :checked="form.needReview" @change="onNeedReviewChange" />
  63. </view>
  64. <!-- TASK-SYS-248: 联动动作 -->
  65. <view class="form-item">
  66. <text class="label">联动动作(可选)</text>
  67. <picker :range="actionTypes" range-key="label" @change="onActionTypeChange">
  68. <view class="picker">
  69. {{ actionTypeLabel || '无(手动完成)' }}
  70. </view>
  71. </picker>
  72. <text class="form-hint">选择后任务按钮将跳转对应功能页</text>
  73. </view>
  74. <view class="form-item" v-if="form.actionType === 'cart' || form.actionType === 'order'">
  75. <text class="label">商品ID(购物任务)</text>
  76. <input class="input" type="number" v-model="form.actionConfig" placeholder="请输入商品ID,如 123" />
  77. <text class="form-hint">购物任务:点开始自动加购并跳转购物车</text>
  78. </view>
  79. <view class="form-item">
  80. <text class="label">需先开始再完成(两阶段)</text>
  81. <switch :checked="form.startRequired" @change="onStartRequiredChange" />
  82. <text class="form-hint">小游戏/专注/购物等任务建议开启</text>
  83. </view>
  84. <view class="form-item">
  85. <text class="label">需填写内容后完成</text>
  86. <switch :checked="form.requireInput" @change="onRequireInputChange" />
  87. </view>
  88. <view class="form-item">
  89. <text class="label">预计时长(分钟)</text>
  90. <input class="input" type="number" v-model="form.duration" placeholder="请输入预计完成时长" />
  91. </view>
  92. <button class="btn-primary" :loading="submitting" :disabled="submitting" @click="submit">创建任务</button>
  93. </view>
  94. </view>
  95. </template>
  96. <script>
  97. import config from '@/config.js'
  98. import { getChildren, createTask } from '../../utils/api.js'
  99. export default {
  100. data() {
  101. return {
  102. form: {
  103. title: '',
  104. description: '',
  105. memberIds: [],
  106. points: 2,
  107. category: '',
  108. minigameCode: '',
  109. deadline: null,
  110. repeatType: 'none',
  111. needReview: false,
  112. duration: null,
  113. actionType: '',
  114. actionConfig: '',
  115. requireInput: false,
  116. startRequired: false
  117. },
  118. children: [],
  119. selectedChildIds: [],
  120. categories: ['学习类', '生活类', '运动类', '小游戏类', '其他'],
  121. miniGames: [],
  122. selectedMinigame: null,
  123. repeatTypes: [
  124. { value: 'none', label: '不重复' },
  125. { value: 'daily', label: '每天' },
  126. { value: 'weekly', label: '每周' },
  127. { value: 'every_other_day', label: '隔天' },
  128. { value: 'monthly', label: '每月' }
  129. ],
  130. actionTypes: [
  131. { value: '', label: '无(手动完成)' },
  132. { value: 'minigame', label: '小游戏' },
  133. { value: 'focus', label: '专注训练' },
  134. { value: 'survey', label: '问卷/测评' },
  135. { value: 'article', label: '文章阅读' },
  136. { value: 'activity', label: '活动报名' },
  137. { value: 'cart', label: '购物(加购)' },
  138. { value: 'order', label: '购物(支付/收货)' }
  139. ],
  140. dateTimeRange: [[], [], [], [], []],
  141. dateTimeIndex: [0, 0, 0, 0, 0],
  142. deadlineText: '',
  143. submitting: false
  144. }
  145. },
  146. computed: {
  147. repeatTypeLabel() {
  148. const found = this.repeatTypes.find(r => r.value === this.form.repeatType)
  149. return found ? found.label : ''
  150. },
  151. actionTypeLabel() {
  152. const found = this.actionTypes.find(a => a.value === this.form.actionType)
  153. return found ? found.label : ''
  154. }
  155. },
  156. onLoad(options) {
  157. this.loadChildren(options.memberId)
  158. this.loadMiniGames()
  159. this.initDateTimePicker()
  160. },
  161. methods: {
  162. async loadChildren(preselectedChildId) {
  163. try {
  164. const res = await getChildren()
  165. this.children = res.data || []
  166. // 如果有 preselect memberId,自动选中该孩子
  167. if (preselectedChildId && this.children.length > 0) {
  168. const matched = this.children.find(c => String(c.id) === String(preselectedChildId))
  169. if (matched) {
  170. const idNum = Number(matched.id)
  171. if (!this.form.memberIds.includes(idNum)) {
  172. this.form.memberIds.push(idNum)
  173. this.selectedChildIds.push(String(idNum))
  174. }
  175. }
  176. }
  177. } catch (e) {
  178. console.error(e)
  179. }
  180. },
  181. async loadMiniGames() {
  182. try {
  183. const res = await uni.request({
  184. url: config.api('/api/tasks/minigame-options'),
  185. method: 'POST',
  186. header: {
  187. 'Authorization': `Bearer ${uni.getStorageSync('token')}`
  188. }
  189. })
  190. if (res.data && res.data.code === 200) {
  191. this.miniGames = res.data.data || []
  192. }
  193. } catch (e) {
  194. console.error(e)
  195. }
  196. },
  197. initDateTimePicker() {
  198. // 初始化日期时间选择器
  199. const now = new Date()
  200. const year = now.getFullYear()
  201. const month = now.getMonth() + 1
  202. const day = now.getDate()
  203. const hour = now.getHours()
  204. const minute = now.getMinutes()
  205. // 年(当前年份+未来2年)
  206. const years = []
  207. for (let i = year; i <= year + 2; i++) {
  208. years.push(i + '年')
  209. }
  210. // 月
  211. const months = []
  212. for (let i = 1; i <= 12; i++) {
  213. months.push(i + '月')
  214. }
  215. // 日
  216. const days = []
  217. const daysInMonth = new Date(year, month, 0).getDate()
  218. for (let i = 1; i <= daysInMonth; i++) {
  219. days.push(i + '日')
  220. }
  221. // 时
  222. const hours = []
  223. for (let i = 0; i < 24; i++) {
  224. hours.push(i.toString().padStart(2, '0') + '时')
  225. }
  226. // 分
  227. const minutes = []
  228. for (let i = 0; i < 60; i += 5) {
  229. minutes.push(i.toString().padStart(2, '0') + '分')
  230. }
  231. this.dateTimeRange = [years, months, days, hours, minutes]
  232. // 默认选中当前日期时间
  233. this.dateTimeIndex = [0, month - 1, day - 1, hour, Math.floor(minute / 5)]
  234. },
  235. onChildrenChange(e) {
  236. this.selectedChildIds = e.detail.value
  237. this.form.memberIds = e.detail.value.map(Number)
  238. },
  239. onCategoryChange(e) {
  240. this.form.category = this.categories[e.detail.value]
  241. // 清空小游戏选择
  242. if (this.form.category !== '小游戏类') {
  243. this.form.minigameCode = ''
  244. this.selectedMinigame = null
  245. }
  246. },
  247. onMinigameChange(e) {
  248. this.selectedMinigame = this.miniGames[e.detail.value]
  249. this.form.minigameCode = this.selectedMinigame.gameCode
  250. // 自动填充任务名称
  251. if (!this.form.title) {
  252. this.form.title = this.selectedMinigame.gameName
  253. }
  254. // 自动填充积分
  255. if (this.selectedMinigame.defaultPoints) {
  256. this.form.points = this.selectedMinigame.defaultPoints
  257. }
  258. },
  259. onDateTimeChange(e) {
  260. const vals = e.detail.value
  261. this.dateTimeIndex = vals
  262. const now = new Date()
  263. const year = now.getFullYear() + vals[0]
  264. const month = vals[1] + 1
  265. const day = vals[2] + 1
  266. const hour = vals[3]
  267. const minute = vals[4] * 5
  268. this.deadlineText = `${year}年${month}月${day}日 ${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`
  269. this.form.deadline = new Date(year, month - 1, day, hour, minute)
  270. },
  271. onColumnChange(e) {
  272. const column = e.detail.column
  273. this.$set(this.dateTimeIndex, column, e.detail.value)
  274. // 年、月变化时更新当月天数(含闰年处理)
  275. if (column === 0 || column === 1) {
  276. const now = new Date()
  277. const year = now.getFullYear() + this.dateTimeIndex[0]
  278. const month = this.dateTimeIndex[1] + 1
  279. const daysInMonth = new Date(year, month, 0).getDate()
  280. const days = []
  281. for (let i = 1; i <= daysInMonth; i++) {
  282. days.push(i + '日')
  283. }
  284. this.$set(this.dateTimeRange, 2, days)
  285. // 若选中的日超出新月天数,回退到最后一天
  286. if (this.dateTimeIndex[2] >= daysInMonth) {
  287. this.$set(this.dateTimeIndex, 2, daysInMonth - 1)
  288. }
  289. }
  290. },
  291. onRepeatChange(e) {
  292. const selected = this.repeatTypes[e.detail.value]
  293. this.form.repeatType = selected.value
  294. },
  295. onNeedReviewChange(e) {
  296. this.form.needReview = e.detail.value
  297. },
  298. onActionTypeChange(e) {
  299. const selected = this.actionTypes[e.detail.value]
  300. this.form.actionType = selected.value
  301. if (selected.value !== 'cart' && selected.value !== 'order') {
  302. this.form.actionConfig = ''
  303. }
  304. },
  305. onStartRequiredChange(e) {
  306. this.form.startRequired = e.detail.value
  307. },
  308. onRequireInputChange(e) {
  309. this.form.requireInput = e.detail.value
  310. },
  311. async submit() {
  312. if (this.submitting) return
  313. if (!this.form.title) {
  314. uni.showToast({ title: '请输入任务名称', icon: 'none' })
  315. return
  316. }
  317. if (this.form.memberIds.length === 0) {
  318. uni.showToast({ title: '请选择至少一个孩子', icon: 'none' })
  319. return
  320. }
  321. if (!this.form.deadline) {
  322. uni.showToast({ title: '请选择截止时间', icon: 'none' })
  323. return
  324. }
  325. // 小游戏类必须选择具体游戏
  326. if (this.form.category === '小游戏类' && !this.form.minigameCode) {
  327. uni.showToast({ title: '请选择具体小游戏', icon: 'none' })
  328. return
  329. }
  330. this.submitting = true
  331. uni.showLoading({ title: '创建任务中...' })
  332. const taskData = {
  333. title: this.form.title,
  334. description: this.form.description,
  335. points: parseInt(this.form.points) || 2,
  336. category: this.form.category || '其他',
  337. minigameCode: this.form.minigameCode,
  338. deadline: this.form.deadline.toISOString(),
  339. repeatType: this.form.repeatType,
  340. frequency: this.form.repeatType,
  341. needReview: this.form.needReview ? 1 : 0,
  342. duration: parseInt(this.form.duration) || 0,
  343. // TASK-SYS-248: 行为调度中心字段
  344. actionType: this.form.actionType || '',
  345. actionConfig: this.form.actionConfig || '',
  346. requireInput: this.form.requireInput ? 1 : 0,
  347. startRequired: this.form.startRequired ? 1 : 0
  348. }
  349. let successCount = 0
  350. let failCount = 0
  351. try {
  352. for (const memberId of this.form.memberIds) {
  353. try {
  354. await createTask({ ...taskData, memberId })
  355. successCount++
  356. } catch (e) {
  357. failCount++
  358. }
  359. }
  360. } finally {
  361. this.submitting = false
  362. uni.hideLoading()
  363. }
  364. if (failCount === 0) {
  365. uni.showToast({ title: `已为${successCount}个孩子创建任务`, icon: 'success' })
  366. setTimeout(() => uni.navigateBack(), 1500)
  367. } else if (successCount > 0) {
  368. uni.showToast({ title: `${successCount}个成功,${failCount}个失败`, icon: 'none' })
  369. } else {
  370. uni.showToast({ title: '创建失败', icon: 'none' })
  371. }
  372. }
  373. }
  374. }
  375. </script>
  376. <style scoped>
  377. .container { padding: 30rpx; }
  378. .form-container { background: #fff; border-radius: 20rpx; padding: 30rpx; }
  379. .form-item { margin-bottom: 30rpx; }
  380. .label { display: block; font-size: 28rpx; color: #333; margin-bottom: 10rpx; }
  381. .input, .textarea { border: 1rpx solid #ddd; border-radius: 10rpx; padding: 20rpx; font-size: 28rpx; width: 100%; box-sizing: border-box; }
  382. .input { height: 96rpx; }
  383. .textarea { height: 150rpx; }
  384. .picker { border: 1rpx solid #ddd; border-radius: 10rpx; padding: 20rpx; font-size: 28rpx; color: #666; }
  385. .form-hint { display: block; font-size: 22rpx; color: #999; margin-top: 8rpx; }
  386. .child-checkbox { display: flex; align-items: center; padding: 16rpx 0; border-bottom: 1rpx solid #f0f0f0; }
  387. .child-checkbox:last-child { border-bottom: none; }
  388. .child-name { font-size: 28rpx; color: #333; margin-left: 10rpx; }
  389. .btn-primary { background: #F97316; color: #fff; border-radius: 20rpx; margin-top: 40rpx; }
  390. </style>