GuideFamilyTask.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. <template>
  2. <div class="page-container">
  3. <el-card>
  4. <div slot="header">
  5. <span>家庭任务管理</span>
  6. <el-button style="float: right" size="small" @click="loadData" :loading="loading">刷新</el-button>
  7. </div>
  8. <!-- 搜索 -->
  9. <el-form :inline="true" :model="searchForm" style="margin-bottom: 20px">
  10. <el-form-item label="选择家庭">
  11. <el-select v-model="searchForm.familyId" placeholder="请选择家庭" clearable @change="handleFamilyChange">
  12. <el-option v-for="family in families" :key="family.familyId" :label="family.familyName" :value="family.familyId">
  13. </el-option>
  14. </el-select>
  15. </el-form-item>
  16. <el-form-item label="选择孩子">
  17. <el-select v-model="searchForm.childId" placeholder="请选择孩子" clearable @change="loadChildTasks">
  18. <el-option v-for="child in children" :key="child.childId" :label="child.childName" :value="child.childId">
  19. </el-option>
  20. </el-select>
  21. </el-form-item>
  22. <el-form-item label="状态">
  23. <el-select v-model="searchForm.status" placeholder="全部" clearable @change="loadChildTasks">
  24. <el-option label="待完成" value="pending"></el-option>
  25. <el-option label="已完成" value="completed"></el-option>
  26. <el-option label="已超时" value="overdue"></el-option>
  27. </el-select>
  28. </el-form-item>
  29. <el-form-item>
  30. <el-button type="primary" @click="loadChildTasks">搜索</el-button>
  31. </el-form-item>
  32. </el-form>
  33. <!-- 家庭概览 -->
  34. <div v-if="familyOverview" class="overview-cards" v-loading="overviewLoading">
  35. <div v-if="!overviewLoading && familyOverview.totalChildren === 0" class="empty-state">
  36. <i class="el-icon-user-solid" style="font-size: 48px; color: #ccc; margin-bottom: 16px;"></i>
  37. <p>暂无孩子数据</p>
  38. </div>
  39. <template v-else>
  40. <el-row :gutter="20">
  41. <el-col :span="6">
  42. <div class="overview-card">
  43. <div class="overview-value">{{ familyOverview.totalChildren }}</div>
  44. <div class="overview-label">孩子数量</div>
  45. </div>
  46. </el-col>
  47. <el-col :span="6">
  48. <div class="overview-card">
  49. <div class="overview-value warning">{{ familyOverview.totalPendingReview }}</div>
  50. <div class="overview-label">待审核任务</div>
  51. </div>
  52. </el-col>
  53. <el-col :span="6">
  54. <div class="overview-card">
  55. <div class="overview-value success">{{ familyOverview.totalCompleted }}</div>
  56. <div class="overview-label">已完成任务</div>
  57. </div>
  58. </el-col>
  59. <el-col :span="6">
  60. <div class="overview-card">
  61. <div class="overview-value danger">{{ familyOverview.totalOverdue }}</div>
  62. <div class="overview-label">超时任务</div>
  63. </div>
  64. </el-col>
  65. </el-row>
  66. </template>
  67. </div>
  68. <!-- 孩子列表 -->
  69. <div style="overflow:auto;max-height:calc(100vh - 300px);">
  70. <el-table style="max-height:calc(100vh - 300px);" v-if="familyOverview && !searchForm.childId" :data="familyOverview.children" border>
  71. <el-table-column prop="childName" label="孩子姓名" width="120"></el-table-column>
  72. <el-table-column prop="totalPoints" label="总积分" width="100"></el-table-column>
  73. <el-table-column prop="streakDays" label="连续打卡" width="100">
  74. <template slot-scope="{ row }">{{ row.streakDays }}天</template>
  75. </el-table-column>
  76. <el-table-column prop="todayTaskCount" label="今日任务" width="100"></el-table-column>
  77. <el-table-column prop="todayCompletedCount" label="今日完成" width="100"></el-table-column>
  78. <el-table-column prop="pendingReviewCount" label="待审核" width="100">
  79. <template slot-scope="{ row }">
  80. <el-badge :value="row.pendingReviewCount" :max="99" class="badge-item" />
  81. </template>
  82. </el-table-column>
  83. <el-table-column label="操作" width="150">
  84. <template slot-scope="{ row }">
  85. <el-button size="mini" @click="selectChild(row)">查看任务</el-button>
  86. </template>
  87. </el-table-column>
  88. </el-table></div>
  89. <!-- 任务列表 -->
  90. <div style="overflow:auto;max-height:calc(100vh - 300px);">
  91. <el-table v-if="searchForm.childId" :data="tasks" border v-loading="taskLoading">
  92. <template slot="empty">
  93. <div class="empty-table-state">
  94. <i class="el-icon-tickets" style="font-size: 48px; color: #ccc; margin-bottom: 16px;"></i>
  95. <p>暂无任务数据</p>
  96. </div>
  97. </template>
  98. <el-table-column prop="id" label="ID" width="60"></el-table-column>
  99. <el-table-column prop="title" label="任务标题" min-width="150"></el-table-column>
  100. <el-table-column prop="points" label="积分" width="80"></el-table-column>
  101. <el-table-column prop="deadline" label="截止时间" width="160">
  102. <template slot-scope="{ row }">{{ formatDate(row.deadline) }}</template>
  103. </el-table-column>
  104. <el-table-column prop="status" label="状态" width="100">
  105. <template slot-scope="{ row }">
  106. <el-tag :type="getStatusType(row.status)" size="small">{{ getStatusText(row.status) }}</el-tag>
  107. </template>
  108. </el-table-column>
  109. <el-table-column prop="needReview" label="需审核" width="80">
  110. <template slot-scope="{ row }">
  111. <el-tag :type="row.needReview === 1 ? 'warning' : 'info'" size="small">{{ row.needReview === 1 ? '是' : '否' }}</el-tag>
  112. </template>
  113. </el-table-column>
  114. <el-table-column prop="completedAt" label="完成时间" width="160">
  115. <template slot-scope="{ row }">{{ formatDate(row.completedAt) }}</template>
  116. </el-table-column>
  117. <el-table-column label="操作" width="220" fixed="right">
  118. <template slot-scope="{ row }">
  119. <el-button v-if="row.status !== 'cancelled'" size="mini" type="primary" @click="handleEditTask(row)">编辑</el-button>
  120. <el-dropdown trigger="hover" @command="(cmd) => handleActionCmd(row, cmd)">
  121. <el-button size="mini">
  122. 更多<i class="el-icon-arrow-down el-icon--right"></i>
  123. </el-button>
  124. <el-dropdown-menu slot="dropdown">
  125. <el-dropdown-item v-if="row.status !== 'cancelled'" command="delete" icon="el-icon-delete">删除</el-dropdown-item>
  126. <el-dropdown-item v-if="row.status === 'completed' && row.needReview === 1" command="reviewPass" icon="el-icon-check">通过</el-dropdown-item>
  127. <el-dropdown-item v-if="row.status === 'completed' && row.needReview === 1" command="reviewReject" icon="el-icon-close">拒绝</el-dropdown-item>
  128. </el-dropdown-menu>
  129. </el-dropdown>
  130. </template>
  131. </el-table-column>
  132. </el-table></div>
  133. <!-- 分页 -->
  134. <el-pagination
  135. v-if="searchForm.childId && total > 0"
  136. style="margin-top: 20px; text-align: right"
  137. :current-page="pagination.page"
  138. :page-size="pagination.size"
  139. :total="total"
  140. layout="total, prev, pager, next"
  141. @current-change="handlePageChange"
  142. />
  143. </el-card>
  144. <!-- 审核弹窗 -->
  145. <el-dialog title="审核任务" :visible.sync="reviewDialogVisible" width="400px">
  146. <el-form>
  147. <el-form-item label="任务标题">{{ currentTask.title }}</el-form-item>
  148. <el-form-item label="执行者">孩子</el-form-item>
  149. <el-form-item label="完成时间">{{ formatDate(currentTask.completedAt) }}</el-form-item>
  150. <el-form-item label="审核意见">
  151. <el-input type="textarea" v-model="reviewComment" rows="3" placeholder="请输入审核意见(选填)"></el-input>
  152. </el-form-item>
  153. </el-form>
  154. <div slot="footer">
  155. <el-button @click="reviewDialogVisible = false">取消</el-button>
  156. <el-button type="danger" @click="confirmReview(false)">拒绝</el-button>
  157. <el-button type="primary" @click="confirmReview(true)">通过</el-button>
  158. </div>
  159. </el-dialog>
  160. <!-- 编辑任务弹窗 -->
  161. <el-dialog title="编辑任务" :visible.sync="editDialogVisible" width="500px">
  162. <el-form :model="editForm" label-width="80px">
  163. <el-form-item label="任务标题">
  164. <el-input v-model="editForm.title"></el-input>
  165. </el-form-item>
  166. <el-form-item label="任务描述">
  167. <el-input type="textarea" v-model="editForm.description" rows="3"></el-input>
  168. </el-form-item>
  169. <el-form-item label="积分">
  170. <el-input-number v-model="editForm.points" :min="1" :max="100"></el-input-number>
  171. </el-form-item>
  172. <el-form-item label="截止时间">
  173. <el-date-picker v-model="editForm.deadline" type="datetime" placeholder="选择日期时间" value-format="yyyy-MM-dd HH:mm:ss"></el-date-picker>
  174. </el-form-item>
  175. <el-form-item label="状态">
  176. <el-select v-model="editForm.status">
  177. <el-option label="待完成" value="pending"></el-option>
  178. <el-option label="已完成" value="completed"></el-option>
  179. <el-option label="已超时" value="overdue"></el-option>
  180. <el-option label="已取消" value="cancelled"></el-option>
  181. </el-select>
  182. </el-form-item>
  183. </el-form>
  184. <div slot="footer">
  185. <el-button @click="editDialogVisible = false">取消</el-button>
  186. <el-button type="primary" @click="submitEditTask" :loading="editLoading">保存</el-button>
  187. </div>
  188. </el-dialog>
  189. </div>
  190. </template>
  191. <script>
  192. import { fetchBoundFamilies, fetchFamilyOverview, fetchChildTasks, fetchPendingReviewTasks, reviewTask, updateTask, deleteTask } from '@/api/guideTask'
  193. export default {
  194. name: 'GuideFamilyTask',
  195. data() {
  196. return {
  197. loading: false,
  198. taskLoading: false,
  199. overviewLoading: false,
  200. families: [],
  201. children: [],
  202. tasks: [],
  203. familyOverview: null,
  204. total: 0,
  205. searchForm: {
  206. familyId: null,
  207. childId: null,
  208. status: ''
  209. },
  210. pagination: {
  211. page: 1,
  212. size: 10
  213. },
  214. reviewDialogVisible: false,
  215. currentTask: null,
  216. reviewComment: '',
  217. reviewAction: true,
  218. editDialogVisible: false,
  219. editLoading: false,
  220. editForm: {
  221. id: null,
  222. title: '',
  223. description: '',
  224. points: 2,
  225. deadline: '',
  226. status: 'pending'
  227. }
  228. }
  229. },
  230. mounted() {
  231. this.loadFamilies()
  232. },
  233. methods: {
  234. handleActionCmd(row, cmd) {
  235. switch (cmd) {
  236. case 'edit': this.handleEditTask(row); break;
  237. case 'delete': this.handleDeleteTask(row); break;
  238. case 'reviewPass': this.handleReview(row, true); break;
  239. case 'reviewReject': this.handleReview(row, false); break;
  240. }
  241. },
  242. async loadFamilies() {
  243. this.loading = true
  244. try {
  245. const res = await fetchBoundFamilies()
  246. this.families = res.data || []
  247. } catch (e) {
  248. this.$message.error('加载失败')
  249. } finally {
  250. this.loading = false
  251. }
  252. },
  253. async handleFamilyChange() {
  254. if (!this.searchForm.familyId) {
  255. this.familyOverview = null
  256. this.children = []
  257. return
  258. }
  259. this.overviewLoading = true
  260. try {
  261. const res = await fetchFamilyOverview(this.searchForm.familyId)
  262. this.familyOverview = res.data
  263. this.children = res.data.children || []
  264. } catch (e) {
  265. this.$message.error('加载失败')
  266. } finally {
  267. this.overviewLoading = false
  268. }
  269. },
  270. selectChild(child) {
  271. this.searchForm.childId = child.childId
  272. this.loadChildTasks()
  273. },
  274. async loadChildTasks() {
  275. if (!this.searchForm.childId) {
  276. this.tasks = []
  277. return
  278. }
  279. this.taskLoading = true
  280. try {
  281. const res = await fetchChildTasks(
  282. this.searchForm.familyId,
  283. this.searchForm.childId,
  284. this.pagination.page,
  285. this.pagination.size,
  286. this.searchForm.status
  287. )
  288. this.tasks = res.data.records || []
  289. this.total = res.data.total || 0
  290. } catch (e) {
  291. this.$message.error('加载失败')
  292. } finally {
  293. this.taskLoading = false
  294. }
  295. },
  296. handlePageChange(page) {
  297. this.pagination.page = page
  298. this.loadChildTasks()
  299. },
  300. getStatusType(status) {
  301. const map = { pending: 'info', completed: 'success', overdue: 'danger', cancelled: 'info' }
  302. return map[status] || 'info'
  303. },
  304. getStatusText(status) {
  305. const map = { pending: '待完成', completed: '已完成', overdue: '已超时', cancelled: '已取消' }
  306. return map[status] || status
  307. },
  308. formatDate(date) {
  309. if (!date) return '-'
  310. return new Date(date).toLocaleString()
  311. },
  312. handleReview(task, approved) {
  313. this.currentTask = task
  314. this.reviewAction = approved
  315. this.reviewComment = ''
  316. this.reviewDialogVisible = true
  317. },
  318. async confirmReview() {
  319. try {
  320. await reviewTask(
  321. this.searchForm.familyId,
  322. this.currentTask.id,
  323. {
  324. approved: this.reviewAction,
  325. comment: this.reviewComment
  326. }
  327. )
  328. this.$message.success(this.reviewAction ? '已通过' : '已拒绝')
  329. this.reviewDialogVisible = false
  330. this.loadChildTasks()
  331. this.handleFamilyChange()
  332. } catch (e) {
  333. this.$message.error('操作失败')
  334. }
  335. },
  336. handleEditTask(task) {
  337. this.currentTask = task
  338. this.editForm = {
  339. id: task.id,
  340. title: task.title,
  341. description: task.description || '',
  342. points: task.points || 2,
  343. deadline: task.deadline ? new Date(task.deadline).toISOString().slice(0, 16) : '',
  344. status: task.status
  345. }
  346. this.editDialogVisible = true
  347. },
  348. async submitEditTask() {
  349. if (!this.editForm.title) {
  350. this.$message.warning('请输入任务标题')
  351. return
  352. }
  353. this.editLoading = true
  354. try {
  355. const data = {
  356. title: this.editForm.title,
  357. description: this.editForm.description,
  358. points: this.editForm.points,
  359. status: this.editForm.status,
  360. deadline: this.editForm.deadline ? new Date(this.editForm.deadline).getTime() : null
  361. }
  362. await updateTask(this.searchForm.familyId, this.editForm.id, data)
  363. this.$message.success('保存成功')
  364. this.editDialogVisible = false
  365. this.loadChildTasks()
  366. } catch (e) {
  367. this.$message.error('保存失败')
  368. } finally {
  369. this.editLoading = false
  370. }
  371. },
  372. async handleDeleteTask(task) {
  373. try {
  374. await this.$confirm('确定要删除该任务吗?', '提示', {
  375. type: 'warning'
  376. })
  377. await deleteTask(this.searchForm.familyId, task.id)
  378. this.$message.success('删除成功')
  379. this.loadChildTasks()
  380. } catch (e) {
  381. if (e !== 'cancel') {
  382. this.$message.error('删除失败')
  383. }
  384. }
  385. }
  386. }
  387. }
  388. </script>
  389. <style scoped>
  390. .page-container { padding: 20px; }
  391. .overview-cards { margin-bottom: 20px; }
  392. .overview-card {
  393. background: #fff;
  394. border-radius: 8px;
  395. padding: 20px;
  396. text-align: center;
  397. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  398. }
  399. .overview-value {
  400. font-size: 32px;
  401. font-weight: bold;
  402. color: #333;
  403. }
  404. .overview-value.warning { color: #e6a23c; }
  405. .overview-value.success { color: #67c23a; }
  406. .overview-value.danger { color: #f56c6c; }
  407. .overview-label {
  408. font-size: 14px;
  409. color: #999;
  410. margin-top: 8px;
  411. }
  412. .badge-item { margin-top: 8px; }
  413. .empty-state,
  414. .empty-table-state {
  415. text-align: center;
  416. padding: 40px;
  417. color: #999;
  418. background: #fafafa;
  419. border-radius: 8px;
  420. border: 1px dashed #ddd;
  421. }
  422. .empty-table-state {
  423. margin: 20px 0;
  424. }
  425. </style>