tasks.vue 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069
  1. <template>
  2. <view class="container">
  3. <!-- 顶部:家庭成员选择(选择谁就展示谁的任务) -->
  4. <FamilyMemberStrip
  5. v-if="familyMembers.length > 0"
  6. :members="familyMembers"
  7. :selectedMemberId="currentMemberId"
  8. :isParent="isParent"
  9. @select="onFamilyMemberSelect"
  10. />
  11. <!-- Tab 切换 -->
  12. <view class="tab-bar">
  13. <view class="tab-item" :class="{ active: activeTab === 'pending' }" @click="switchTab('pending')">
  14. <text class="tab-text">待接受</text>
  15. <text class="tab-badge" v-if="pendingTasks.length > 0">{{ pendingTasks.length }}</text>
  16. </view>
  17. <view class="tab-item" :class="{ active: activeTab === 'today' }" @click="switchTab('today')">
  18. <text class="tab-text">今日任务</text>
  19. </view>
  20. <view class="tab-item" :class="{ active: activeTab === 'created' }" @click="switchTab('created')">
  21. <text class="tab-text">我创建的</text>
  22. </view>
  23. </view>
  24. <!-- 待接受任务 -->
  25. <view class="task-list" v-if="activeTab === 'pending'">
  26. <view class="task-item assigned" v-for="task in pendingTasks" :key="task.id">
  27. <view class="task-left">
  28. <view class="task-title">
  29. {{ task.title }}
  30. <text v-if="task.memberOnly === 1" class="member-badge">会员任务</text>
  31. </view>
  32. <view class="task-meta">
  33. <text class="points">+{{ task.points }}CF</text>
  34. <text class="creator">创建人:{{ task.creatorName || '家人' }}</text>
  35. <text class="deadline">截止 {{ formatTime(task.deadline) }}</text>
  36. </view>
  37. </view>
  38. <view class="task-actions">
  39. <button class="btn-accept" @click="acceptTask(task)">接受</button>
  40. <button class="btn-reject" @click="showRejectModal(task)">拒绝</button>
  41. </view>
  42. </view>
  43. <view class="empty" v-if="!loading && pendingTasks.length === 0">
  44. <text>暂无待接受任务</text>
  45. </view>
  46. </view>
  47. <!-- 今日任务 -->
  48. <view class="task-list" v-if="activeTab === 'today'">
  49. <view class="task-item" v-for="task in todayTasks" :key="task.id" :class="{ completed: task.status === 'completed', locked: task.status === 'locked' }">
  50. <view class="task-left">
  51. <view class="task-title">
  52. {{ task.title }}
  53. <text v-if="task.memberOnly === 1" class="member-badge">会员任务</text>
  54. <text v-if="task.category === '小游戏类'" class="minigame-badge">🎮</text>
  55. <text v-if="task.actionType === 'cart' || task.actionType === 'order'" class="minigame-badge">🛒</text>
  56. <text v-if="task.actionType === 'focus'" class="minigame-badge">⏱️</text>
  57. <text v-if="task.actionType === 'survey'" class="minigame-badge">📝</text>
  58. <text v-if="task.actionType === 'article'" class="minigame-badge">📖</text>
  59. <text v-if="task.actionType === 'activity'" class="minigame-badge">🎯</text>
  60. <text v-if="task.requireInput === 1" class="minigame-badge">✍️</text>
  61. </view>
  62. <view class="task-meta">
  63. <text class="points">+{{ task.points }}CF</text>
  64. <text class="deadline">截止 {{ formatTime(task.deadline) }}</text>
  65. </view>
  66. <!-- 进度型任务:展示进度条 -->
  67. <view class="progress-wrap" v-if="task.isDailyProgress === 1">
  68. <view class="progress-bar">
  69. <view class="progress-fill" :style="{ width: getProgressPercent(task) + '%' }"></view>
  70. </view>
  71. <text class="progress-text">{{ task.progress || 0 }}/{{ task.targetValue || 1 }}</text>
  72. </view>
  73. <text v-if="task.status === 'locked'" class="lock-tip">🔒 需先完成前置任务后解锁</text>
  74. </view>
  75. <view class="task-right" v-if="task.status === 'pending' || task.status === 'in_progress'">
  76. <button class="btn-complete" @click="handleTaskAction(task)">{{ getActionLabel(task) }}</button>
  77. </view>
  78. <view class="completed-badge" v-else-if="task.status === 'completed'">已完成</view>
  79. <view class="review-badge" v-else-if="task.status === 'pending_review'">待审核</view>
  80. <view class="lock-badge" v-else>🔒 未解锁</view>
  81. </view>
  82. <view class="empty" v-if="!loading && todayTasks.length === 0">
  83. <text>暂无今日任务</text>
  84. </view>
  85. </view>
  86. <!-- 我创建的任务 -->
  87. <view class="task-list" v-if="activeTab === 'created'">
  88. <view class="task-item" v-for="task in createdTasks" :key="task.id">
  89. <view class="task-left">
  90. <view class="task-title">
  91. {{ task.title }}
  92. <text v-if="task.memberOnly === 1" class="member-badge">会员任务</text>
  93. </view>
  94. <view class="task-meta">
  95. <text class="points">+{{ task.points }}CF</text>
  96. <text class="assignee">{{ task.executorName || '未分配' }}</text>
  97. <text class="status-tag" :class="'status-' + task.status">{{ statusLabel(task.status) }}</text>
  98. </view>
  99. </view>
  100. </view>
  101. <view class="empty" v-if="!loading && createdTasks.length === 0">
  102. <text>暂无创建的任务</text>
  103. </view>
  104. </view>
  105. <!-- 添加任务按钮 -->
  106. <button v-if="activeTab === 'today' && isParent" class="add-btn" @click="showAddModal = true">
  107. + 添加任务
  108. </button>
  109. <!-- 流程编排入口 -->
  110. <button v-if="activeTab === 'today' && isParent" class="orchestration-btn" @click="goToOrchestration">
  111. 📋 流程编排
  112. </button>
  113. <!-- 拒绝确认弹窗 -->
  114. <view class="modal-mask" v-if="showRejectModalFlag" @click="showRejectModalFlag = false">
  115. <view class="modal" @click.stop>
  116. <view class="modal-title">确认拒绝</view>
  117. <view class="modal-content">
  118. <text>拒绝后,创建人对你的信任度和亲密度将各减1分,沟通质量各加1分。</text>
  119. </view>
  120. <view class="modal-btns">
  121. <button @click="showRejectModalFlag = false">取消</button>
  122. <button class="btn-primary" @click="confirmReject">确认拒绝</button>
  123. </view>
  124. </view>
  125. </view>
  126. <!-- 填写式完成弹窗 -->
  127. <view class="modal-mask" v-if="showInputModal" @click="closeInputModal">
  128. <view class="modal modal-large" @click.stop>
  129. <view class="modal-title">完成任务</view>
  130. <view class="modal-content">
  131. <text class="input-tip">{{ activeInputTask && activeInputTask.title }}</text>
  132. <textarea
  133. v-model="inputContent"
  134. class="input-textarea"
  135. placeholder="请填写完成内容(如:今天完成了...)"
  136. :maxlength="500"
  137. ></textarea>
  138. </view>
  139. <view class="modal-btns">
  140. <button @click="closeInputModal">取消</button>
  141. <button class="btn-primary" :disabled="!inputContent" @click="submitWithInput">提交完成</button>
  142. </view>
  143. </view>
  144. </view>
  145. <!-- 创建任务弹窗 -->
  146. <view class="modal-mask" v-if="showAddModal" @click="showAddModal = false">
  147. <view class="modal modal-large" @click.stop>
  148. <view class="modal-title">创建任务</view>
  149. <view class="form-item">
  150. <view class="form-label">任务标题 <text class="required">*</text></view>
  151. <input v-model="newTask.title" placeholder="请输入任务标题" />
  152. </view>
  153. <view class="form-item">
  154. <view class="form-label">任务内容</view>
  155. <textarea v-model="newTask.description" placeholder="请输入任务描述" class="textarea" />
  156. </view>
  157. <view class="form-item">
  158. <view class="form-label">类型 <text class="required">*</text></view>
  159. <picker mode="selector" :range="categories" @change="onCategoryChange">
  160. <view class="picker">{{ newTask.category || '请选择类型' }}</view>
  161. </picker>
  162. </view>
  163. <view class="form-item">
  164. <view class="form-label">积分 <text class="required">*</text></view>
  165. <input v-model="newTask.points" type="number" placeholder="1-10" />
  166. </view>
  167. <view class="form-item">
  168. <view class="form-label">截止时间 <text class="required">*</text></view>
  169. <picker mode="datetime" :value="newTask.deadline" @change="onDeadlineChange">
  170. <view class="picker">{{ newTask.deadline || '选择截止时间' }}</view>
  171. </picker>
  172. </view>
  173. <view class="form-item">
  174. <view class="form-label">分配给 <text class="required">*</text></view>
  175. <checkbox-group @change="onFamilyMemberSelectChange">
  176. <label v-for="member in familyMembers" :key="member.id" :class="{ 'label-checked': newTask.memberIds.includes(String(member.id)) }">
  177. <checkbox :value="String(member.id)" :checked="newTask.memberIds.includes(String(member.id))" />
  178. <text>{{ member.nickname }}</text>
  179. </label>
  180. </checkbox-group>
  181. </view>
  182. <view class="modal-btns">
  183. <button @click="showAddModal = false">取消</button>
  184. <button class="btn-primary" @click="createTask">创建</button>
  185. </view>
  186. </view>
  187. </view>
  188. </view>
  189. </template>
  190. <script>
  191. import { getMyPendingTasks, acceptTask as acceptTaskApi, rejectTask as rejectTaskApi, getTodayTasks, completeTask as completeTaskApi, startTask as startTaskApi, createTask as createTaskApi, getFamilyMemberList, getMyMembership, reportAppEnter
  192. } from '../../utils/api.js'
  193. import { parseDate } from '../../utils/format.js'
  194. import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
  195. export default {
  196. components: { FamilyMemberStrip },
  197. data() {
  198. return {
  199. activeTab: 'today',
  200. loading: false,
  201. pendingTasks: [],
  202. todayTasks: [],
  203. createdTasks: [],
  204. isParent: false,
  205. isMember: false,
  206. currentMemberId: null,
  207. currentUserId: null,
  208. familyMembers: [],
  209. showAddModal: false,
  210. showRejectModalFlag: false,
  211. rejectTaskId: null,
  212. showInputModal: false,
  213. activeInputTask: null,
  214. inputContent: '',
  215. newTask: {
  216. title: '',
  217. description: '',
  218. category: '',
  219. points: 2,
  220. deadline: '',
  221. memberIds: []
  222. },
  223. categories: ['学习类', '生活类', '运动类', '其他']
  224. }
  225. },
  226. onLoad(options) {
  227. reportAppEnter('task');
  228. if (options && options.tab) {
  229. this.activeTab = options.tab
  230. }
  231. if (options && options.memberId) {
  232. var mid = parseInt(options.memberId, 10)
  233. if (!isNaN(mid)) {
  234. this.currentMemberId = mid
  235. uni.setStorageSync('currentMemberId', mid)
  236. uni.setStorageSync('currentChildId', mid)
  237. }
  238. }
  239. },
  240. onShow() {
  241. const role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
  242. this.isParent = role === 'parent'
  243. this.currentUserId = uni.getStorageSync('userId')
  244. if (this.currentMemberId == null) {
  245. var cached = uni.getStorageSync('currentMemberId') || uni.getStorageSync('currentChildId')
  246. if (cached !== '' && cached != null) {
  247. var n = parseInt(cached, 10)
  248. this.currentMemberId = isNaN(n) ? cached : n
  249. }
  250. }
  251. this.checkMembership()
  252. this.loadInitialData()
  253. },
  254. methods: {
  255. onFamilyMemberSelect(member) {
  256. if (!member || member.id == null) return
  257. var id = typeof member.id === 'number' ? member.id : parseInt(member.id, 10)
  258. if (isNaN(id)) id = member.id
  259. if (this.currentMemberId === id) return
  260. this.currentMemberId = id
  261. uni.setStorageSync('currentMemberId', id)
  262. uni.setStorageSync('currentChildId', id)
  263. this.loadTodayTasks()
  264. this.loadPendingTasks()
  265. },
  266. resolveTaskMemberId(task) {
  267. if (task && task.familyMemberId) return task.familyMemberId
  268. if (task && task.childId) return task.childId
  269. return this.currentMemberId
  270. },
  271. checkMembership() {
  272. getMyMembership().then(res => {
  273. const m = res.data || {}
  274. this.isMember = !!(m.levelCode && m.levelCode !== 'FREE')
  275. }).catch(() => {})
  276. },
  277. switchTab(tab) {
  278. this.activeTab = tab
  279. if (tab === 'pending') this.loadPendingTasks()
  280. else if (tab === 'today') this.loadTodayTasks()
  281. else if (tab === 'created') this.loadCreatedTasks()
  282. },
  283. goToOrchestration() {
  284. uni.navigateTo({ url: '/pages/orchestration/index' })
  285. },
  286. async loadInitialData() {
  287. this.loading = true
  288. try {
  289. const membersRes = await getFamilyMemberList({ visibleOnly: true })
  290. var raw = (membersRes && membersRes.data) || []
  291. this.familyMembers = Array.isArray(raw) ? raw : []
  292. this.ensureSelectedMemberFromRaw(this.familyMembers)
  293. if (!this.currentMemberId) {
  294. this.todayTasks = []
  295. this.pendingTasks = []
  296. } else {
  297. this.loadTodayTasks()
  298. this.loadPendingTasks()
  299. }
  300. this.loadCreatedTasks()
  301. } catch (e) {
  302. console.error('加载数据失败', e)
  303. } finally {
  304. this.loading = false
  305. }
  306. },
  307. ensureSelectedMemberFromRaw(raw) {
  308. var members = raw || []
  309. if (members.length === 0) {
  310. this.currentMemberId = null
  311. return
  312. }
  313. if (this.currentMemberId != null) {
  314. var cur = String(this.currentMemberId)
  315. for (var i = 0; i < members.length; i++) {
  316. if (members[i] && String(members[i].id) === cur) {
  317. // 统一成数字,保证 FamilyMemberStrip 选中态 === 比较成立
  318. var n = parseInt(members[i].id, 10)
  319. this.currentMemberId = isNaN(n) ? members[i].id : n
  320. return
  321. }
  322. }
  323. }
  324. var first = members[0]
  325. var fid = first && first.id != null ? first.id : null
  326. if (fid != null) {
  327. var pn = parseInt(fid, 10)
  328. this.currentMemberId = isNaN(pn) ? fid : pn
  329. uni.setStorageSync('currentMemberId', this.currentMemberId)
  330. uni.setStorageSync('currentChildId', this.currentMemberId)
  331. } else {
  332. this.currentMemberId = null
  333. }
  334. },
  335. async loadPendingTasks() {
  336. if (!this.currentMemberId) {
  337. this.pendingTasks = []
  338. return
  339. }
  340. try {
  341. const res = await getMyPendingTasks(this.currentMemberId)
  342. this.pendingTasks = res.data || []
  343. } catch (e) {
  344. console.error('加载待接受任务失败', e)
  345. }
  346. },
  347. async loadTodayTasks() {
  348. if (!this.currentMemberId) {
  349. this.todayTasks = []
  350. return
  351. }
  352. try {
  353. const res = await getTodayTasks(this.currentMemberId)
  354. this.todayTasks = res.data || []
  355. } catch (e) {
  356. console.error('加载今日任务失败', e)
  357. }
  358. },
  359. async loadCreatedTasks() {
  360. try {
  361. const res = await getTodayTasks(this.currentUserId)
  362. this.createdTasks = res.data || []
  363. } catch (e) {
  364. console.error('加载创建的任务失败', e)
  365. }
  366. },
  367. async completeTask(task) {
  368. if (task.memberOnly === 1 && !this.isMember) {
  369. uni.showModal({
  370. title: '会员任务',
  371. content: '该任务是会员专属任务,开通会员后即可完成',
  372. confirmText: '去开通',
  373. success: (res) => {
  374. if (res.confirm) {
  375. uni.navigateTo({ url: '/pages/membership/index' })
  376. }
  377. }
  378. })
  379. return
  380. }
  381. try {
  382. var memberId = this.resolveTaskMemberId(task)
  383. const res = await completeTaskApi(task.id, memberId, '')
  384. if (res.data && res.data.needReview) {
  385. uni.showToast({ title: '已提交,待审核', icon: 'none' })
  386. } else {
  387. uni.showToast({ title: `获得 ${res.data.pointsEarned} CF`, icon: 'success' })
  388. }
  389. this.loadTodayTasks()
  390. } catch (e) {
  391. console.error('完成任务失败', e)
  392. }
  393. },
  394. /** 按钮文案:按 action_type / start_required / require_input 分派 */
  395. getActionLabel(task) {
  396. if (task.startRequired === 1 && task.status !== 'in_progress') return '开始'
  397. if (task.requireInput === 1) return '填写完成'
  398. var at = task.actionType
  399. if (at === 'minigame') return '开始游戏'
  400. if (at === 'focus') return '开始专注'
  401. if (at === 'survey') return '去填写'
  402. if (at === 'article') return '去阅读'
  403. if (at === 'activity') return '去报名'
  404. if (at === 'cart' || at === 'order') return '去购物'
  405. if (at === 'diet') return '去打卡'
  406. if (at === 'sleep') return '去打卡'
  407. if (at === 'exercise') return '去打卡'
  408. return '完成'
  409. },
  410. /** 统一任务动作入口:开始/联动跳转/填写/直接完成 */
  411. handleTaskAction(task) {
  412. var self = this
  413. // 前置锁定任务不可操作
  414. if (task.status === 'locked') {
  415. uni.showToast({ title: '需先完成前置任务', icon: 'none' })
  416. return
  417. }
  418. // 两阶段:先开始,再按 action_type 跳转联动页面
  419. if (task.startRequired === 1 && task.status !== 'in_progress') {
  420. uni.showLoading({ title: '开始中...' })
  421. startTaskApi(task.id, this.resolveTaskMemberId(task)).then(function(res) {
  422. uni.hideLoading()
  423. if (res && res.code === 200 && res.data) {
  424. uni.showToast({ title: '已开始', icon: 'success' })
  425. self.navigateByAction(task)
  426. self.loadTodayTasks()
  427. } else {
  428. uni.showToast({ title: (res && res.message) || '开始失败', icon: 'none' })
  429. }
  430. }).catch(function() {
  431. uni.hideLoading()
  432. uni.showToast({ title: '开始失败', icon: 'none' })
  433. })
  434. return
  435. }
  436. // 填写式完成
  437. if (task.requireInput === 1) {
  438. this.activeInputTask = task
  439. this.inputContent = ''
  440. this.showInputModal = true
  441. return
  442. }
  443. // 打卡/联动类任务(含每日进度型打卡任务):跳转打卡页执行,
  444. // 打卡提交后由后端回写(一次性任务直接完成 / 每日任务累计进度)
  445. if (task.actionType === 'diet' || task.actionType === 'sleep' || task.actionType === 'exercise') {
  446. this.navigateByAction(task)
  447. return
  448. }
  449. // 已开始的两阶段任务或普通任务:直接完成
  450. this.completeTask(task)
  451. },
  452. /** 按 action_type 跳转联动页面 */
  453. navigateByAction(task) {
  454. var at = task.actionType
  455. var taskId = task.id
  456. if (at === 'minigame') {
  457. var cfg = {}
  458. try { cfg = typeof task.actionConfig === 'string' ? JSON.parse(task.actionConfig || '{}') : (task.actionConfig || {}) } catch (e) {}
  459. var gameId = cfg.gameId || ''
  460. uni.navigateTo({ url: '/pages/games/index?taskId=' + taskId + (gameId ? '&gameId=' + gameId : '') })
  461. } else if (at === 'focus') {
  462. uni.navigateTo({ url: '/pages/games/focus-timer?taskId=' + taskId })
  463. } else if (at === 'survey') {
  464. uni.navigateTo({ url: '/pages/survey/index?taskId=' + taskId })
  465. } else if (at === 'article') {
  466. uni.navigateTo({ url: '/pages/article-center/index?taskId=' + taskId })
  467. } else if (at === 'activity') {
  468. uni.navigateTo({ url: '/pages/activity/list?taskId=' + taskId })
  469. } else if (at === 'cart' || at === 'order') {
  470. uni.navigateTo({ url: '/pages/shop/cart?taskId=' + taskId })
  471. } else if (at === 'diet') {
  472. uni.navigateTo({ url: '/pages/growth/diet-checkin?taskId=' + taskId })
  473. } else if (at === 'sleep') {
  474. uni.navigateTo({ url: '/pages/growth/sleep-checkin?taskId=' + taskId })
  475. } else if (at === 'exercise') {
  476. uni.navigateTo({ url: '/pages/growth/exercise-checkin?taskId=' + taskId })
  477. }
  478. },
  479. closeInputModal() {
  480. this.showInputModal = false
  481. this.activeInputTask = null
  482. this.inputContent = ''
  483. },
  484. async submitWithInput() {
  485. var task = this.activeInputTask
  486. if (!task || !this.inputContent) return
  487. try {
  488. const res = await completeTaskApi(task.id, this.resolveTaskMemberId(task), '', this.inputContent, 'text')
  489. if (res.data && res.data.needReview) {
  490. uni.showToast({ title: '已提交,待审核', icon: 'none' })
  491. } else {
  492. uni.showToast({ title: `获得 ${res.data.pointsEarned} CF`, icon: 'success' })
  493. }
  494. this.closeInputModal()
  495. this.loadTodayTasks()
  496. } catch (e) {
  497. console.error('提交填写内容失败', e)
  498. uni.showToast({ title: '提交失败', icon: 'none' })
  499. }
  500. },
  501. acceptTask(task) {
  502. if (task.memberOnly === 1 && !this.isMember) {
  503. uni.showModal({
  504. title: '会员任务',
  505. content: '该任务是会员专属任务,开通会员后即可接受',
  506. confirmText: '去开通',
  507. success: (res) => {
  508. if (res.confirm) {
  509. uni.navigateTo({ url: '/pages/membership/index' })
  510. }
  511. }
  512. })
  513. return
  514. }
  515. uni.showLoading({ title: '接受中...' })
  516. acceptTaskApi(task.id, this.currentMemberId).then(res => {
  517. uni.hideLoading()
  518. uni.showToast({ title: '已接受', icon: 'success' })
  519. this.loadPendingTasks()
  520. this.loadTodayTasks()
  521. }).catch(e => {
  522. uni.hideLoading()
  523. console.error('接受失败', e)
  524. uni.showToast({ title: '接受失败', icon: 'none' })
  525. })
  526. },
  527. showRejectModal(task) {
  528. this.rejectTaskId = task.id
  529. this.showRejectModalFlag = true
  530. },
  531. confirmReject() {
  532. uni.showLoading({ title: '拒绝中...' })
  533. rejectTaskApi(this.rejectTaskId, this.currentMemberId).then(res => {
  534. uni.hideLoading()
  535. this.showRejectModalFlag = false
  536. uni.showToast({ title: '已拒绝', icon: 'success' })
  537. this.loadPendingTasks()
  538. }).catch(e => {
  539. uni.hideLoading()
  540. console.error('拒绝失败', e)
  541. uni.showToast({ title: '拒绝失败', icon: 'none' })
  542. })
  543. },
  544. statusLabel(status) {
  545. const map = {
  546. pending: '待完成',
  547. assigned: '待接受',
  548. in_progress: '进行中',
  549. completed: '已完成',
  550. pending_review: '待审核',
  551. rejected: '已拒绝',
  552. overdue: '已过期',
  553. cancelled: '已取消',
  554. locked: '未解锁'
  555. }
  556. return map[status] || status
  557. },
  558. formatTime(t) {
  559. if (!t) return ''
  560. var d = parseDate(t)
  561. if (!d) return String(t).substring(0, 16)
  562. var y = d.getFullYear()
  563. var m = String(d.getMonth() + 1).padStart(2, '0')
  564. var day = String(d.getDate()).padStart(2, '0')
  565. var h = String(d.getHours()).padStart(2, '0')
  566. var min = String(d.getMinutes()).padStart(2, '0')
  567. return y + '-' + m + '-' + day + ' ' + h + ':' + min
  568. },
  569. getProgressPercent(task) {
  570. var progress = task.progress || 0
  571. var target = task.targetValue || 1
  572. if (target <= 0) return 0
  573. var pct = Math.round((progress / target) * 100)
  574. return pct > 100 ? 100 : pct
  575. },
  576. async createTask() {
  577. if (!this.newTask.title) {
  578. uni.showToast({ title: '请输入标题', icon: 'none' })
  579. return
  580. }
  581. if (!this.newTask.category) {
  582. uni.showToast({ title: '请选择类型', icon: 'none' })
  583. return
  584. }
  585. if (this.newTask.memberIds.length === 0) {
  586. uni.showToast({ title: '请选择分配对象', icon: 'none' })
  587. return
  588. }
  589. try {
  590. await createTaskApi({
  591. title: this.newTask.title,
  592. description: this.newTask.description,
  593. category: this.newTask.category,
  594. points: parseInt(this.newTask.points) || 2,
  595. deadline: this.newTask.deadline ? new Date(this.newTask.deadline).getTime() : null,
  596. executorType: 'member',
  597. memberIds: this.newTask.memberIds.map(id => Number(id))
  598. })
  599. uni.showToast({ title: '创建成功', icon: 'success' })
  600. this.showAddModal = false
  601. this.resetNewTask()
  602. this.loadCreatedTasks()
  603. this.loadPendingTasks()
  604. } catch (e) {
  605. console.error('创建任务失败', e)
  606. uni.showToast({ title: '创建失败', icon: 'none' })
  607. }
  608. },
  609. resetNewTask() {
  610. this.newTask = {
  611. title: '',
  612. description: '',
  613. category: '',
  614. points: 2,
  615. deadline: '',
  616. memberIds: []
  617. }
  618. },
  619. onCategoryChange(e) {
  620. this.newTask.category = this.categories[e.detail.value]
  621. },
  622. onDeadlineChange(e) {
  623. this.newTask.deadline = e.detail.value
  624. },
  625. onFamilyMemberSelectChange(e) {
  626. this.newTask.memberIds = e.detail.value || []
  627. }
  628. }
  629. }
  630. </script>
  631. <style scoped>
  632. .container {
  633. padding: 20rpx 30rpx 120rpx;
  634. background: #f5f7fa;
  635. min-height: 100vh;
  636. }
  637. .tab-bar {
  638. margin-top: 16rpx;
  639. display: flex;
  640. background: #fff;
  641. border-radius: 16rpx;
  642. padding: 10rpx;
  643. margin-bottom: 30rpx;
  644. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  645. }
  646. .tab-item {
  647. flex: 1;
  648. display: flex;
  649. align-items: center;
  650. justify-content: center;
  651. padding: 20rpx 0;
  652. border-radius: 12rpx;
  653. position: relative;
  654. }
  655. .tab-item.active {
  656. background: linear-gradient(135deg, #F97316, #FB923C);
  657. }
  658. .tab-text {
  659. font-size: 28rpx;
  660. color: #666;
  661. }
  662. .tab-item.active .tab-text {
  663. color: #fff;
  664. font-weight: 600;
  665. }
  666. .tab-badge {
  667. position: absolute;
  668. top: 8rpx;
  669. right: 20rpx;
  670. background: #EF4444;
  671. color: #fff;
  672. font-size: 20rpx;
  673. min-width: 28rpx;
  674. height: 28rpx;
  675. line-height: 28rpx;
  676. text-align: center;
  677. border-radius: 14rpx;
  678. padding: 0 6rpx;
  679. }
  680. .task-list {
  681. display: flex;
  682. flex-direction: column;
  683. gap: 20rpx;
  684. }
  685. .task-item {
  686. display: flex;
  687. align-items: center;
  688. justify-content: space-between;
  689. background: #fff;
  690. border-radius: 20rpx;
  691. padding: 28rpx;
  692. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
  693. }
  694. .task-item.completed {
  695. opacity: 0.7;
  696. }
  697. .task-item.locked {
  698. opacity: 0.55;
  699. }
  700. .task-item.assigned {
  701. border-left: 6rpx solid #F97316;
  702. }
  703. .task-left {
  704. flex: 1;
  705. min-width: 0;
  706. padding-right: 20rpx;
  707. }
  708. .task-title {
  709. font-size: 30rpx;
  710. color: #333;
  711. font-weight: 500;
  712. margin-bottom: 12rpx;
  713. }
  714. .member-badge {
  715. font-size: 20rpx;
  716. color: #F97316;
  717. background: #FFF7ED;
  718. padding: 2rpx 10rpx;
  719. border-radius: 8rpx;
  720. margin-left: 8rpx;
  721. }
  722. .minigame-badge {
  723. margin-left: 8rpx;
  724. font-size: 24rpx;
  725. }
  726. .task-meta {
  727. display: flex;
  728. flex-wrap: wrap;
  729. gap: 16rpx;
  730. font-size: 24rpx;
  731. color: #999;
  732. }
  733. .points {
  734. color: #F97316;
  735. font-weight: 500;
  736. }
  737. .deadline, .creator, .assignee {
  738. color: #999;
  739. }
  740. .lock-tip {
  741. display: block;
  742. margin-top: 8rpx;
  743. font-size: 22rpx;
  744. color: #9CA3AF;
  745. }
  746. .progress-wrap {
  747. display: flex;
  748. align-items: center;
  749. margin-top: 12rpx;
  750. }
  751. .progress-bar {
  752. flex: 1;
  753. height: 12rpx;
  754. background: #F3F4F6;
  755. border-radius: 6rpx;
  756. overflow: hidden;
  757. margin-right: 12rpx;
  758. }
  759. .progress-fill {
  760. height: 100%;
  761. background: linear-gradient(135deg, #F97316, #FB923C);
  762. border-radius: 6rpx;
  763. transition: width 0.3s ease;
  764. }
  765. .progress-text {
  766. font-size: 22rpx;
  767. color: #F97316;
  768. white-space: nowrap;
  769. }
  770. .task-right {
  771. flex-shrink: 0;
  772. }
  773. .btn-complete {
  774. background: linear-gradient(135deg, #F97316, #FB923C);
  775. color: #fff;
  776. font-size: 26rpx;
  777. padding: 0 28rpx;
  778. height: 64rpx;
  779. line-height: 64rpx;
  780. border-radius: 32rpx;
  781. border: none;
  782. }
  783. .task-actions {
  784. display: flex;
  785. gap: 12rpx;
  786. flex-shrink: 0;
  787. }
  788. .btn-accept {
  789. background: #10B981;
  790. color: #fff;
  791. font-size: 24rpx;
  792. padding: 0 24rpx;
  793. height: 56rpx;
  794. line-height: 56rpx;
  795. border-radius: 28rpx;
  796. border: none;
  797. }
  798. .btn-reject {
  799. background: #F3F4F6;
  800. color: #666;
  801. font-size: 24rpx;
  802. padding: 0 24rpx;
  803. height: 56rpx;
  804. line-height: 56rpx;
  805. border-radius: 28rpx;
  806. border: none;
  807. }
  808. .completed-badge, .review-badge, .lock-badge {
  809. font-size: 24rpx;
  810. padding: 8rpx 16rpx;
  811. border-radius: 12rpx;
  812. flex-shrink: 0;
  813. }
  814. .completed-badge {
  815. background: #ECFDF5;
  816. color: #10B981;
  817. }
  818. .review-badge {
  819. background: #FFF7ED;
  820. color: #F97316;
  821. }
  822. .lock-badge {
  823. background: #F3F4F6;
  824. color: #9CA3AF;
  825. }
  826. .status-tag {
  827. font-size: 22rpx;
  828. padding: 2rpx 10rpx;
  829. border-radius: 8rpx;
  830. background: #F3F4F6;
  831. color: #666;
  832. }
  833. .status-pending { background: #FFF7ED; color: #F97316; }
  834. .status-completed { background: #ECFDF5; color: #10B981; }
  835. .status-pending_review { background: #FEF3C7; color: #D97706; }
  836. .status-assigned { background: #EEF2FF; color: #6366F1; }
  837. .status-in_progress { background: #DBEAFE; color: #2563EB; }
  838. .status-rejected { background: #FEE2E2; color: #EF4444; }
  839. .status-locked { background: #F3F4F6; color: #9CA3AF; }
  840. .empty {
  841. display: flex;
  842. justify-content: center;
  843. align-items: center;
  844. padding: 80rpx 0;
  845. color: #9CA3AF;
  846. font-size: 28rpx;
  847. }
  848. .add-btn {
  849. position: fixed;
  850. left: 40rpx;
  851. right: 40rpx;
  852. bottom: 40rpx;
  853. height: 88rpx;
  854. line-height: 88rpx;
  855. background: linear-gradient(135deg, #F97316, #FB923C);
  856. color: #fff;
  857. font-size: 30rpx;
  858. border-radius: 44rpx;
  859. border: none;
  860. box-shadow: 0 8rpx 24rpx rgba(249, 115, 22, 0.35);
  861. }
  862. .orchestration-btn {
  863. position: fixed;
  864. left: 40rpx;
  865. right: 40rpx;
  866. bottom: 148rpx;
  867. height: 88rpx;
  868. line-height: 88rpx;
  869. background: linear-gradient(135deg, #6366F1, #8B5CF6);
  870. color: #fff;
  871. font-size: 30rpx;
  872. border-radius: 44rpx;
  873. border: none;
  874. box-shadow: 0 8rpx 24rpx rgba(99, 102, 241, 0.35);
  875. }
  876. .modal-mask {
  877. position: fixed;
  878. top: 0;
  879. left: 0;
  880. right: 0;
  881. bottom: 0;
  882. background: rgba(0, 0, 0, 0.45);
  883. display: flex;
  884. align-items: center;
  885. justify-content: center;
  886. z-index: 1000;
  887. }
  888. .modal {
  889. background: #fff;
  890. border-radius: 28rpx;
  891. padding: 40rpx;
  892. width: 85%;
  893. max-height: 80vh;
  894. overflow-y: auto;
  895. box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.12);
  896. }
  897. .modal-large {
  898. width: 90%;
  899. }
  900. .modal-title {
  901. font-size: 34rpx;
  902. font-weight: bold;
  903. margin-bottom: 32rpx;
  904. text-align: center;
  905. color: #333;
  906. }
  907. .modal-content {
  908. font-size: 28rpx;
  909. color: #666;
  910. line-height: 1.6;
  911. margin-bottom: 30rpx;
  912. text-align: center;
  913. }
  914. .input-tip {
  915. display: block;
  916. margin-bottom: 16rpx;
  917. color: #333;
  918. font-weight: 500;
  919. }
  920. .input-textarea {
  921. width: 100%;
  922. min-height: 180rpx;
  923. padding: 20rpx;
  924. background: #F9FAFB;
  925. border-radius: 12rpx;
  926. font-size: 28rpx;
  927. box-sizing: border-box;
  928. }
  929. .modal-btns {
  930. display: flex;
  931. gap: 20rpx;
  932. margin-top: 30rpx;
  933. }
  934. .modal-btns button {
  935. flex: 1;
  936. height: 88rpx;
  937. line-height: 88rpx;
  938. border-radius: 44rpx;
  939. font-size: 28rpx;
  940. border: none;
  941. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
  942. }
  943. .modal-btns button:first-child {
  944. background: #f5f5f5;
  945. color: #666;
  946. }
  947. .btn-primary {
  948. background: linear-gradient(135deg, #F97316, #FB923C) !important;
  949. color: #fff !important;
  950. }
  951. .form-item {
  952. margin-bottom: 28rpx;
  953. }
  954. .form-label {
  955. font-size: 28rpx;
  956. color: #333;
  957. margin-bottom: 14rpx;
  958. font-weight: 500;
  959. }
  960. .form-label .required {
  961. color: #F97316;
  962. }
  963. .form-item input,
  964. .form-item .picker,
  965. .form-item .textarea {
  966. width: 100%;
  967. padding: 20rpx;
  968. background: #F9FAFB;
  969. border-radius: 12rpx;
  970. font-size: 28rpx;
  971. box-sizing: border-box;
  972. }
  973. .form-item .textarea {
  974. min-height: 140rpx;
  975. }
  976. .form-item checkbox-group {
  977. display: flex;
  978. flex-wrap: wrap;
  979. gap: 16rpx;
  980. }
  981. .form-item label {
  982. display: flex;
  983. align-items: center;
  984. gap: 8rpx;
  985. padding: 12rpx 20rpx;
  986. background: #F9FAFB;
  987. border-radius: 12rpx;
  988. font-size: 26rpx;
  989. }
  990. .form-item label.label-checked {
  991. background: #FFF7ED;
  992. color: #F97316;
  993. }
  994. </style>