GuideFamilyTask.vue 19 KB

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