GuideFamilyTask.vue 16 KB

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