GuideFamilyTask.vue 16 KB

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