create-task.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  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],
  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. onChildrenChange(e) {
  234. this.selectedChildIds = e.detail.value
  235. this.form.memberIds = e.detail.value.map(Number)
  236. },
  237. onCategoryChange(e) {
  238. this.form.category = this.categories[e.detail.value]
  239. // 清空小游戏选择
  240. if (this.form.category !== '小游戏类') {
  241. this.form.minigameCode = ''
  242. this.selectedMinigame = null
  243. }
  244. },
  245. onMinigameChange(e) {
  246. this.selectedMinigame = this.miniGames[e.detail.value]
  247. this.form.minigameCode = this.selectedMinigame.gameCode
  248. // 自动填充任务名称
  249. if (!this.form.title) {
  250. this.form.title = this.selectedMinigame.gameName
  251. }
  252. // 自动填充积分
  253. if (this.selectedMinigame.defaultPoints) {
  254. this.form.points = this.selectedMinigame.defaultPoints
  255. }
  256. },
  257. onDateTimeChange(e) {
  258. const vals = e.detail.value
  259. this.dateTimeIndex = vals
  260. const now = new Date()
  261. const year = now.getFullYear() + vals[0]
  262. const month = vals[1] + 1
  263. const day = vals[2] + 1
  264. const hour = vals[3]
  265. const minute = vals[4] * 5
  266. this.deadlineText = `${year}年${month}月${day}日 ${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`
  267. this.form.deadline = new Date(year, month - 1, day, hour, minute)
  268. },
  269. onColumnChange(e) {
  270. // 处理月份变化时更新天数
  271. if (e.detail.column === 1) {
  272. const month = e.detail.value + 1
  273. const now = new Date()
  274. const year = now.getFullYear() + this.dateTimeIndex[0]
  275. const daysInMonth = new Date(year, month, 0).getDate()
  276. const days = []
  277. for (let i = 1; i <= daysInMonth; i++) {
  278. days.push(i + '日')
  279. }
  280. this.$set(this.dateTimeRange, 2, days)
  281. }
  282. },
  283. onRepeatChange(e) {
  284. const selected = this.repeatTypes[e.detail.value]
  285. this.form.repeatType = selected.value
  286. },
  287. onNeedReviewChange(e) {
  288. this.form.needReview = e.detail.value
  289. },
  290. onActionTypeChange(e) {
  291. const selected = this.actionTypes[e.detail.value]
  292. this.form.actionType = selected.value
  293. if (selected.value !== 'cart' && selected.value !== 'order') {
  294. this.form.actionConfig = ''
  295. }
  296. },
  297. onStartRequiredChange(e) {
  298. this.form.startRequired = e.detail.value
  299. },
  300. onRequireInputChange(e) {
  301. this.form.requireInput = e.detail.value
  302. },
  303. async submit() {
  304. if (this.submitting) return
  305. if (!this.form.title) {
  306. uni.showToast({ title: '请输入任务名称', icon: 'none' })
  307. return
  308. }
  309. if (this.form.memberIds.length === 0) {
  310. uni.showToast({ title: '请选择至少一个孩子', icon: 'none' })
  311. return
  312. }
  313. if (!this.form.deadline) {
  314. uni.showToast({ title: '请选择截止时间', icon: 'none' })
  315. return
  316. }
  317. // 小游戏类必须选择具体游戏
  318. if (this.form.category === '小游戏类' && !this.form.minigameCode) {
  319. uni.showToast({ title: '请选择具体小游戏', icon: 'none' })
  320. return
  321. }
  322. this.submitting = true
  323. uni.showLoading({ title: '创建任务中...' })
  324. const taskData = {
  325. title: this.form.title,
  326. description: this.form.description,
  327. points: parseInt(this.form.points) || 2,
  328. category: this.form.category || '其他',
  329. minigameCode: this.form.minigameCode,
  330. deadline: this.form.deadline.toISOString(),
  331. repeatType: this.form.repeatType,
  332. frequency: this.form.repeatType,
  333. needReview: this.form.needReview ? 1 : 0,
  334. duration: parseInt(this.form.duration) || 0,
  335. // TASK-SYS-248: 行为调度中心字段
  336. actionType: this.form.actionType || '',
  337. actionConfig: this.form.actionConfig || '',
  338. requireInput: this.form.requireInput ? 1 : 0,
  339. startRequired: this.form.startRequired ? 1 : 0
  340. }
  341. let successCount = 0
  342. let failCount = 0
  343. try {
  344. for (const memberId of this.form.memberIds) {
  345. try {
  346. await createTask({ ...taskData, memberId })
  347. successCount++
  348. } catch (e) {
  349. failCount++
  350. }
  351. }
  352. } finally {
  353. this.submitting = false
  354. uni.hideLoading()
  355. }
  356. if (failCount === 0) {
  357. uni.showToast({ title: `已为${successCount}个孩子创建任务`, icon: 'success' })
  358. setTimeout(() => uni.navigateBack(), 1500)
  359. } else if (successCount > 0) {
  360. uni.showToast({ title: `${successCount}个成功,${failCount}个失败`, icon: 'none' })
  361. } else {
  362. uni.showToast({ title: '创建失败', icon: 'none' })
  363. }
  364. }
  365. }
  366. }
  367. </script>
  368. <style scoped>
  369. .container { padding: 30rpx; }
  370. .form-container { background: #fff; border-radius: 20rpx; padding: 30rpx; }
  371. .form-item { margin-bottom: 30rpx; }
  372. .label { display: block; font-size: 28rpx; color: #333; margin-bottom: 10rpx; }
  373. .input, .textarea { border: 1rpx solid #ddd; border-radius: 10rpx; padding: 20rpx; font-size: 28rpx; width: 100%; box-sizing: border-box; }
  374. .input { height: 80rpx; }
  375. .textarea { height: 150rpx; }
  376. .picker { border: 1rpx solid #ddd; border-radius: 10rpx; padding: 20rpx; font-size: 28rpx; color: #666; }
  377. .form-hint { display: block; font-size: 22rpx; color: #999; margin-top: 8rpx; }
  378. .child-checkbox { display: flex; align-items: center; padding: 16rpx 0; border-bottom: 1rpx solid #f0f0f0; }
  379. .child-checkbox:last-child { border-bottom: none; }
  380. .child-name { font-size: 28rpx; color: #333; margin-left: 10rpx; }
  381. .btn-primary { background: #F97316; color: #fff; border-radius: 20rpx; margin-top: 40rpx; }
  382. </style>